1# -*- coding: utf-8 -*-
2# This code is part of Ansible, but is an independent component.
3# This particular file snippet, and this file snippet only, is BSD licensed.
4# Modules you write using this snippet, which is embedded dynamically by Ansible
5# still belong to the author of the module, and may assign their own license
6# to the complete work.
7#
8# Copyright (c) 2016 Thomas Krahn (@Nosmoht)
9#
10# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
11
12from __future__ import (absolute_import, division, print_function)
13__metaclass__ = type
14
15import json
16import os
17import socket
18import uuid
19
20import re
21from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
22from ansible.module_utils.six import PY3
23from ansible.module_utils.six.moves.urllib.parse import quote
24from ansible.module_utils.urls import fetch_url, HAS_GSSAPI
25from ansible.module_utils.basic import env_fallback, AnsibleFallbackNotFound
26
27
28def _env_then_dns_fallback(*args, **kwargs):
29    ''' Load value from environment or DNS in that order'''
30    try:
31        result = env_fallback(*args, **kwargs)
32        if result == '':
33            raise AnsibleFallbackNotFound
34    except AnsibleFallbackNotFound:
35        # If no host was given, we try to guess it from IPA.
36        # The ipa-ca entry is a standard entry that IPA will have set for
37        # the CA.
38        try:
39            return socket.gethostbyaddr(socket.gethostbyname('ipa-ca'))[0]
40        except Exception:
41            raise AnsibleFallbackNotFound
42
43
44class IPAClient(object):
45    def __init__(self, module, host, port, protocol):
46        self.host = host
47        self.port = port
48        self.protocol = protocol
49        self.module = module
50        self.headers = None
51        self.timeout = module.params.get('ipa_timeout')
52        self.use_gssapi = False
53
54    def get_base_url(self):
55        return '%s://%s/ipa' % (self.protocol, self.host)
56
57    def get_json_url(self):
58        return '%s/session/json' % self.get_base_url()
59
60    def login(self, username, password):
61        if 'KRB5CCNAME' in os.environ and HAS_GSSAPI:
62            self.use_gssapi = True
63        elif 'KRB5_CLIENT_KTNAME' in os.environ and HAS_GSSAPI:
64            ccache = "MEMORY:" + str(uuid.uuid4())
65            os.environ['KRB5CCNAME'] = ccache
66            self.use_gssapi = True
67        else:
68            if not password:
69                if 'KRB5CCNAME' in os.environ or 'KRB5_CLIENT_KTNAME' in os.environ:
70                    self.module.warn("In order to use GSSAPI, you need to install 'urllib_gssapi'")
71                self._fail('login', 'Password is required if not using '
72                           'GSSAPI. To use GSSAPI, please set the '
73                           'KRB5_CLIENT_KTNAME or KRB5CCNAME (or both) '
74                           ' environment variables.')
75            url = '%s/session/login_password' % self.get_base_url()
76            data = 'user=%s&password=%s' % (quote(username, safe=''), quote(password, safe=''))
77            headers = {'referer': self.get_base_url(),
78                       'Content-Type': 'application/x-www-form-urlencoded',
79                       'Accept': 'text/plain'}
80            try:
81                resp, info = fetch_url(module=self.module, url=url, data=to_bytes(data), headers=headers, timeout=self.timeout)
82                status_code = info['status']
83                if status_code not in [200, 201, 204]:
84                    self._fail('login', info['msg'])
85
86                self.headers = {'Cookie': info.get('set-cookie')}
87            except Exception as e:
88                self._fail('login', to_native(e))
89        if not self.headers:
90            self.headers = dict()
91        self.headers.update({
92            'referer': self.get_base_url(),
93            'Content-Type': 'application/json',
94            'Accept': 'application/json'})
95
96    def _fail(self, msg, e):
97        if 'message' in e:
98            err_string = e.get('message')
99        else:
100            err_string = e
101        self.module.fail_json(msg='%s: %s' % (msg, err_string))
102
103    def get_ipa_version(self):
104        response = self.ping()['summary']
105        ipa_ver_regex = re.compile(r'IPA server version (\d\.\d\.\d).*')
106        version_match = ipa_ver_regex.match(response)
107        ipa_version = None
108        if version_match:
109            ipa_version = version_match.groups()[0]
110        return ipa_version
111
112    def ping(self):
113        return self._post_json(method='ping', name=None)
114
115    def _post_json(self, method, name, item=None):
116        if item is None:
117            item = {}
118        url = '%s/session/json' % self.get_base_url()
119        data = dict(method=method)
120
121        # TODO: We should probably handle this a little better.
122        if method in ('ping', 'config_show', 'otpconfig_show'):
123            data['params'] = [[], {}]
124        elif method in ('config_mod', 'otpconfig_mod'):
125            data['params'] = [[], item]
126        else:
127            data['params'] = [[name], item]
128
129        try:
130            resp, info = fetch_url(module=self.module, url=url, data=to_bytes(json.dumps(data)),
131                                   headers=self.headers, timeout=self.timeout, use_gssapi=self.use_gssapi)
132            status_code = info['status']
133            if status_code not in [200, 201, 204]:
134                self._fail(method, info['msg'])
135        except Exception as e:
136            self._fail('post %s' % method, to_native(e))
137
138        if PY3:
139            charset = resp.headers.get_content_charset('latin-1')
140        else:
141            response_charset = resp.headers.getparam('charset')
142            if response_charset:
143                charset = response_charset
144            else:
145                charset = 'latin-1'
146        resp = json.loads(to_text(resp.read(), encoding=charset))
147        err = resp.get('error')
148        if err is not None:
149            self._fail('response %s' % method, err)
150
151        if 'result' in resp:
152            result = resp.get('result')
153            if 'result' in result:
154                result = result.get('result')
155                if isinstance(result, list):
156                    if len(result) > 0:
157                        return result[0]
158                    else:
159                        return {}
160            return result
161        return None
162
163    def get_diff(self, ipa_data, module_data):
164        result = []
165        for key in module_data.keys():
166            mod_value = module_data.get(key, None)
167            if isinstance(mod_value, list):
168                default = []
169            else:
170                default = None
171            ipa_value = ipa_data.get(key, default)
172            if isinstance(ipa_value, list) and not isinstance(mod_value, list):
173                mod_value = [mod_value]
174            if isinstance(ipa_value, list) and isinstance(mod_value, list):
175                mod_value = sorted(mod_value)
176                ipa_value = sorted(ipa_value)
177            if mod_value != ipa_value:
178                result.append(key)
179        return result
180
181    def modify_if_diff(self, name, ipa_list, module_list, add_method, remove_method, item=None):
182        changed = False
183        diff = list(set(ipa_list) - set(module_list))
184        if len(diff) > 0:
185            changed = True
186            if not self.module.check_mode:
187                if item:
188                    remove_method(name=name, item={item: diff})
189                else:
190                    remove_method(name=name, item=diff)
191
192        diff = list(set(module_list) - set(ipa_list))
193        if len(diff) > 0:
194            changed = True
195            if not self.module.check_mode:
196                if item:
197                    add_method(name=name, item={item: diff})
198                else:
199                    add_method(name=name, item=diff)
200
201        return changed
202
203
204def ipa_argument_spec():
205    return dict(
206        ipa_prot=dict(type='str', default='https', choices=['http', 'https'], fallback=(env_fallback, ['IPA_PROT'])),
207        ipa_host=dict(type='str', default='ipa.example.com', fallback=(_env_then_dns_fallback, ['IPA_HOST'])),
208        ipa_port=dict(type='int', default=443, fallback=(env_fallback, ['IPA_PORT'])),
209        ipa_user=dict(type='str', default='admin', fallback=(env_fallback, ['IPA_USER'])),
210        ipa_pass=dict(type='str', no_log=True, fallback=(env_fallback, ['IPA_PASS'])),
211        ipa_timeout=dict(type='int', default=10, fallback=(env_fallback, ['IPA_TIMEOUT'])),
212        validate_certs=dict(type='bool', default=True),
213    )
214