Blame roles/ipaclient/action_plugins/ipaclient_get_otp.py

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])