|
Packit |
8cb997 |
# Authors:
|
|
Packit |
8cb997 |
# Florence Blanc-Renaud <frenaud@redhat.com>
|
|
Packit |
8cb997 |
#
|
|
Packit |
8cb997 |
# Copyright (C) 2017 Red Hat
|
|
Packit |
8cb997 |
# see file 'COPYING' for use and warranty information
|
|
Packit |
8cb997 |
#
|
|
Packit |
8cb997 |
# This program is free software; you can redistribute it and/or modify
|
|
Packit |
8cb997 |
# it under the terms of the GNU General Public License as published by
|
|
Packit |
8cb997 |
# the Free Software Foundation, either version 3 of the License, or
|
|
Packit |
8cb997 |
# (at your option) any later version.
|
|
Packit |
8cb997 |
#
|
|
Packit |
8cb997 |
# This program is distributed in the hope that it will be useful,
|
|
Packit |
8cb997 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
Packit |
8cb997 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
Packit |
8cb997 |
# GNU General Public License for more details.
|
|
Packit |
8cb997 |
#
|
|
Packit |
8cb997 |
# You should have received a copy of the GNU General Public License
|
|
Packit |
8cb997 |
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
try:
|
|
Packit |
8cb997 |
import gssapi
|
|
Packit |
8cb997 |
except ImportError:
|
|
Packit |
8cb997 |
gssapi = None
|
|
Packit |
8cb997 |
import os
|
|
Packit |
8cb997 |
import shutil
|
|
Packit |
8cb997 |
import subprocess
|
|
Packit |
8cb997 |
import tempfile
|
|
Packit |
8cb997 |
from jinja2 import Template
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
from ansible.errors import AnsibleError
|
|
Packit |
8cb997 |
from ansible.module_utils._text import to_native
|
|
Packit |
8cb997 |
from ansible.plugins.action import ActionBase
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
def run_cmd(args, stdin=None):
|
|
Packit |
8cb997 |
"""
|
|
Packit |
8cb997 |
Execute an external command.
|
|
Packit |
8cb997 |
"""
|
|
Packit |
8cb997 |
p_in = None
|
|
Packit |
8cb997 |
p_out = subprocess.PIPE
|
|
Packit |
8cb997 |
p_err = subprocess.PIPE
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
if stdin:
|
|
Packit |
8cb997 |
p_in = subprocess.PIPE
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
p = subprocess.Popen(args, stdin=p_in, stdout=p_out, stderr=p_err,
|
|
Packit |
8cb997 |
close_fds=True)
|
|
Packit |
8cb997 |
__temp, stderr = p.communicate(stdin)
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
if p.returncode != 0:
|
|
Packit |
8cb997 |
raise RuntimeError(stderr)
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
def kinit_password(principal, password, ccache_name, config):
|
|
Packit |
8cb997 |
"""
|
|
Packit |
8cb997 |
Perform kinit using principal/password, with the specified config file
|
|
Packit |
8cb997 |
and store the TGT in ccache_name.
|
|
Packit |
8cb997 |
"""
|
|
Packit |
8cb997 |
args = ["/usr/bin/kinit", principal, '-c', ccache_name]
|
|
Packit |
8cb997 |
old_config = os.environ.get('KRB5_CONFIG')
|
|
Packit |
8cb997 |
os.environ['KRB5_CONFIG'] = config
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
try:
|
|
Packit |
8cb997 |
return run_cmd(args, stdin=password.encode())
|
|
Packit |
8cb997 |
finally:
|
|
Packit |
8cb997 |
if old_config is not None:
|
|
Packit |
8cb997 |
os.environ['KRB5_CONFIG'] = old_config
|
|
Packit |
8cb997 |
else:
|
|
Packit |
8cb997 |
os.environ.pop('KRB5_CONFIG', None)
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
def kinit_keytab(principal, keytab, ccache_name, config):
|
|
Packit |
8cb997 |
"""
|
|
Packit |
8cb997 |
Perform kinit using principal/keytab, with the specified config file
|
|
Packit |
8cb997 |
and store the TGT in ccache_name.
|
|
Packit |
8cb997 |
"""
|
|
Packit |
8cb997 |
if gssapi is None:
|
|
Packit |
8cb997 |
raise ImportError("gssapi is not available")
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
old_config = os.environ.get('KRB5_CONFIG')
|
|
Packit |
8cb997 |
os.environ['KRB5_CONFIG'] = config
|
|
Packit |
8cb997 |
try:
|
|
Packit |
8cb997 |
name = gssapi.Name(principal, gssapi.NameType.kerberos_principal)
|
|
Packit |
8cb997 |
store = {'ccache': ccache_name,
|
|
Packit |
8cb997 |
'client_keytab': keytab}
|
|
Packit |
8cb997 |
cred = gssapi.Credentials(name=name, store=store, usage='initiate')
|
|
Packit |
8cb997 |
return cred
|
|
Packit |
8cb997 |
finally:
|
|
Packit |
8cb997 |
if old_config is not None:
|
|
Packit |
8cb997 |
os.environ['KRB5_CONFIG'] = old_config
|
|
Packit |
8cb997 |
else:
|
|
Packit |
8cb997 |
os.environ.pop('KRB5_CONFIG', None)
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
KRB5CONF_TEMPLATE = """
|
|
Packit |
8cb997 |
[logging]
|
|
Packit |
8cb997 |
default = FILE:/var/log/krb5libs.log
|
|
Packit |
8cb997 |
kdc = FILE:/var/log/krb5kdc.log
|
|
Packit |
8cb997 |
admin_server = FILE:/var/log/kadmind.log
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
[libdefaults]
|
|
Packit |
8cb997 |
default_realm = {{ ipa_realm }}
|
|
Packit |
8cb997 |
dns_lookup_realm = false
|
|
Packit |
8cb997 |
dns_lookup_kdc = true
|
|
Packit |
8cb997 |
rdns = false
|
|
Packit |
8cb997 |
ticket_lifetime = {{ ipa_lifetime }}
|
|
Packit |
8cb997 |
forwardable = true
|
|
Packit |
8cb997 |
udp_preference_limit = 0
|
|
Packit |
8cb997 |
default_ccache_name = KEYRING:persistent:%{uid}
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
[realms]
|
|
Packit |
8cb997 |
{{ ipa_realm }} = {
|
|
Packit |
8cb997 |
kdc = {{ ipa_server }}:88
|
|
Packit |
8cb997 |
master_kdc = {{ ipa_server }}:88
|
|
Packit |
8cb997 |
admin_server = {{ ipa_server }}:749
|
|
Packit |
8cb997 |
default_domain = {{ ipa_domain }}
|
|
Packit |
8cb997 |
}
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
[domain_realm]
|
|
Packit |
8cb997 |
.{{ ipa_domain }} = {{ ipa_realm }}
|
|
Packit |
8cb997 |
{{ ipa_domain }} = {{ ipa_realm }}
|
|
Packit |
8cb997 |
"""
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
class ActionModule(ActionBase):
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
def run(self, tmp=None, task_vars=None):
|
|
Packit |
8cb997 |
"""
|
|
Packit |
8cb997 |
handler for credential cache transfer
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
ipa* commands can either provide a password or a keytab file
|
|
Packit |
8cb997 |
in order to authenticate on the managed node with Kerberos.
|
|
Packit |
8cb997 |
The module is using these credentials to obtain a TGT locally on the
|
|
Packit |
8cb997 |
control node:
|
|
Packit |
8cb997 |
- need to create a krb5.conf Kerberos client configuration that is
|
|
Packit |
8cb997 |
using IPA server
|
|
Packit |
8cb997 |
- set the environment variable KRB5_CONFIG to point to this conf file
|
|
Packit |
8cb997 |
- set the environment variable KRB5CCNAME to use a specific cache
|
|
Packit |
8cb997 |
- perform kinit on the control node
|
|
Packit |
8cb997 |
This command creates the credential cache file
|
|
Packit |
8cb997 |
- copy the credential cache file on the managed node
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
Then the IPA commands can use this credential cache file.
|
|
Packit |
8cb997 |
"""
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
if task_vars is None:
|
|
Packit |
8cb997 |
task_vars = dict()
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
result = super(ActionModule, self).run(tmp, task_vars)
|
|
Packit |
8cb997 |
principal = self._task.args.get('principal', None)
|
|
Packit |
8cb997 |
keytab = self._task.args.get('keytab', None)
|
|
Packit |
8cb997 |
password = self._task.args.get('password', None)
|
|
Packit |
8cb997 |
lifetime = self._task.args.get('lifetime', '1h')
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
if (not keytab and not password):
|
|
Packit |
8cb997 |
result['failed'] = True
|
|
Packit |
8cb997 |
result['msg'] = "keytab or password is required"
|
|
Packit |
8cb997 |
return result
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
if not principal:
|
|
Packit |
8cb997 |
result['failed'] = True
|
|
Packit |
8cb997 |
result['msg'] = "principal is required"
|
|
Packit |
8cb997 |
return result
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
data = self._execute_module(module_name='ipaclient_get_facts',
|
|
Packit |
8cb997 |
module_args=dict(), task_vars=None)
|
|
Packit |
8cb997 |
try:
|
|
Packit |
8cb997 |
domain = data['ansible_facts']['ipa']['domain']
|
|
Packit |
8cb997 |
realm = data['ansible_facts']['ipa']['realm']
|
|
Packit |
8cb997 |
except KeyError:
|
|
Packit |
8cb997 |
result['failed'] = True
|
|
Packit |
8cb997 |
result['msg'] = "The host is not an IPA server"
|
|
Packit |
8cb997 |
return result
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
items = principal.split('@')
|
|
Packit |
8cb997 |
if len(items) < 2:
|
|
Packit |
8cb997 |
principal = str('%s@%s' % (principal, realm))
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
# Locally create a temp directory to store krb5.conf and ccache
|
|
Packit |
8cb997 |
local_temp_dir = tempfile.mkdtemp()
|
|
Packit |
8cb997 |
krb5conf_name = os.path.join(local_temp_dir, 'krb5.conf')
|
|
Packit |
8cb997 |
ccache_name = os.path.join(local_temp_dir, 'ccache')
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
# Create the krb5.conf from the template
|
|
Packit |
8cb997 |
template = Template(KRB5CONF_TEMPLATE)
|
|
Packit |
8cb997 |
content = template.render(dict(
|
|
Packit |
8cb997 |
ipa_server=task_vars['ansible_host'],
|
|
Packit |
8cb997 |
ipa_domain=domain,
|
|
Packit |
8cb997 |
ipa_realm=realm,
|
|
Packit |
8cb997 |
ipa_lifetime=lifetime))
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
with open(krb5conf_name, 'w') as f:
|
|
Packit |
8cb997 |
f.write(content)
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
if password:
|
|
Packit |
8cb997 |
try:
|
|
Packit |
8cb997 |
# perform kinit -c ccache_name -l 1h principal
|
|
Packit |
8cb997 |
kinit_password(principal, password, ccache_name,
|
|
Packit |
8cb997 |
krb5conf_name)
|
|
Packit |
8cb997 |
except Exception as e:
|
|
Packit |
8cb997 |
result['failed'] = True
|
|
Packit |
8cb997 |
result['msg'] = 'kinit %s with password failed: %s' % \
|
|
Packit |
8cb997 |
(principal, to_native(e))
|
|
Packit |
8cb997 |
return result
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
else:
|
|
Packit |
8cb997 |
# Password not supplied, need to use the keytab file
|
|
Packit |
8cb997 |
# Check if the source keytab exists
|
|
Packit |
8cb997 |
try:
|
|
Packit |
8cb997 |
keytab = self._find_needle('files', keytab)
|
|
Packit |
8cb997 |
except AnsibleError as e:
|
|
Packit |
8cb997 |
result['failed'] = True
|
|
Packit |
8cb997 |
result['msg'] = to_native(e)
|
|
Packit |
8cb997 |
return result
|
|
Packit |
8cb997 |
# perform kinit -kt keytab
|
|
Packit |
8cb997 |
try:
|
|
Packit |
8cb997 |
kinit_keytab(principal, keytab, ccache_name, krb5conf_name)
|
|
Packit |
8cb997 |
except Exception as e:
|
|
Packit |
8cb997 |
result['failed'] = True
|
|
Packit |
8cb997 |
result['msg'] = 'kinit %s with keytab %s failed: %s' % \
|
|
Packit |
8cb997 |
(principal, keytab, str(e))
|
|
Packit |
8cb997 |
return result
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
try:
|
|
Packit |
8cb997 |
# Create the remote tmp dir
|
|
Packit |
8cb997 |
tmp = self._make_tmp_path()
|
|
Packit |
8cb997 |
tmp_ccache = self._connection._shell.join_path(
|
|
Packit |
8cb997 |
tmp, os.path.basename(ccache_name))
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
# Copy the ccache to the remote tmp dir
|
|
Packit |
8cb997 |
self._transfer_file(ccache_name, tmp_ccache)
|
|
Packit |
8cb997 |
self._fixup_perms2((tmp, tmp_ccache))
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
new_module_args = self._task.args.copy()
|
|
Packit |
8cb997 |
new_module_args.pop('password', None)
|
|
Packit |
8cb997 |
new_module_args.pop('keytab', None)
|
|
Packit |
8cb997 |
new_module_args.pop('lifetime', None)
|
|
Packit |
8cb997 |
new_module_args.update(ccache=tmp_ccache)
|
|
Packit |
8cb997 |
|
|
Packit |
8cb997 |
# Execute module
|
|
Packit |
8cb997 |
result.update(self._execute_module(module_args=new_module_args,
|
|
Packit |
8cb997 |
task_vars=task_vars))
|
|
Packit |
8cb997 |
return result
|
|
Packit |
8cb997 |
finally:
|
|
Packit |
8cb997 |
# delete the local temp directory
|
|
Packit |
8cb997 |
shutil.rmtree(local_temp_dir, ignore_errors=True)
|
|
Packit |
8cb997 |
run_cmd(['/usr/bin/kdestroy', '-c', tmp_ccache])
|