Files
ansible-freeipa/action_plugins/ipahost.py
Florence Blanc-Renaud 38d7223376 Modify ipahost module: the authentication is done locally on the controller
node and the credential cache is copied to the managed node

ipahost module is also using facts gathered from the server to find the
domain and realm.
2017-08-10 16:54:44 +02:00

243 lines
8.0 KiB
Python

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