1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3#
4# Copyright: (c) 2018, F5 Networks Inc.
5# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10DOCUMENTATION = r'''
11---
12module: bigip_password_policy
13short_description: Manages the authentication password policy on a BIG-IP
14description:
15  - Manages the authentication password policy on a BIG-IP device.
16version_added: "1.0.0"
17options:
18  expiration_warning:
19    description:
20      - Specifies the number of days before a password expires.
21      - This value determines when the BIG-IP system automatically
22        warns users their password is about to expire.
23    type: int
24  max_duration:
25    description:
26      - Specifies the maximum number of days a password is valid.
27    type: int
28  max_login_failures:
29    description:
30      - Specifies the number of consecutive unsuccessful login attempts
31        the system allows before locking out the user.
32      - Specify zero (0) to disable this parameter.
33    type: int
34  min_duration:
35    description:
36      - Specifies the minimum number of days a password is valid.
37    type: int
38  min_length:
39    description:
40      - Specifies the minimum number of characters in a valid password.
41      - This value must be between 6 and 255.
42    type: int
43  policy_enforcement:
44    description:
45      - Enables or disables the password policy on the BIG-IP system.
46    type: bool
47  required_lowercase:
48    description:
49      - Specifies the number of lowercase alpha characters that must be
50        present in a password for the password to be valid.
51    type: int
52  required_numeric:
53    description:
54      - Specifies the number of numeric characters that must be present in
55        a password for the password to be valid.
56    type: int
57  required_special:
58    description:
59      - Specifies the number of special characters that must be present in
60        a password for the password to be valid.
61    type: int
62  required_uppercase:
63    description:
64      - Specifies the number of uppercase alpha characters that must be
65        present in a password for the password to be valid.
66    type: int
67  password_memory:
68    description:
69      - Specifies whether the user has configured the BIG-IP system to
70        remember a password on a specific computer and how many passwords
71        to remember.
72    type: int
73extends_documentation_fragment: f5networks.f5_modules.f5
74author:
75  - Tim Rupp (@caphrim007)
76'''
77
78EXAMPLES = r'''
79- name: Change password policy to require 2 numeric characters
80  bigip_password_policy:
81    required_numeric: 2
82    provider:
83      password: secret
84      server: lb.mydomain.com
85      user: admin
86  delegate_to: localhost
87'''
88
89RETURN = r'''
90expiration_warning:
91  description: The new expiration warning.
92  returned: changed
93  type: int
94  sample: 7
95max_duration:
96  description: The new max duration.
97  returned: changed
98  type: int
99  sample: 99999
100max_login_failures:
101  description: The new max login failures.
102  returned: changed
103  type: int
104  sample: 0
105min_duration:
106  description: The new minimum duration.
107  returned: changed
108  type: int
109  sample: 0
110min_length:
111  description: The new minimum password length.
112  returned: changed
113  type: int
114  sample: 6
115policy_enforcement:
116  description: The new policy enforcement setting.
117  returned: changed
118  type: bool
119  sample: yes
120required_lowercase:
121  description: The lowercase requirement.
122  returned: changed
123  type: int
124  sample: 1
125required_numeric:
126  description: The numeric requirement.
127  returned: changed
128  type: int
129  sample: 2
130required_special:
131  description: The special character requirement.
132  returned: changed
133  type: int
134  sample: 1
135required_uppercase:
136  description: The uppercase character requirement.
137  returned: changed
138  type: int
139  sample: 1
140password_memory:
141  description: The new number of remembered passwords
142  returned: changed
143  type: int
144  sample: 0
145'''
146from datetime import datetime
147
148from ansible.module_utils.basic import AnsibleModule
149
150from ..module_utils.bigip import F5RestClient
151from ..module_utils.common import (
152    F5ModuleError, AnsibleF5Parameters, f5_argument_spec, flatten_boolean
153)
154from ..module_utils.icontrol import tmos_version
155from ..module_utils.teem import send_teem
156
157
158class Parameters(AnsibleF5Parameters):
159    api_map = {
160        'expirationWarning': 'expiration_warning',
161        'maxDuration': 'max_duration',
162        'maxLoginFailures': 'max_login_failures',
163        'minDuration': 'min_duration',
164        'minimumLength': 'min_length',
165        'passwordMemory': 'password_memory',
166        'policyEnforcement': 'policy_enforcement',
167        'requiredLowercase': 'required_lowercase',
168        'requiredNumeric': 'required_numeric',
169        'requiredSpecial': 'required_special',
170        'requiredUppercase': 'required_uppercase',
171    }
172
173    api_attributes = [
174        'expirationWarning',
175        'maxDuration',
176        'maxLoginFailures',
177        'minDuration',
178        'minimumLength',
179        'passwordMemory',
180        'policyEnforcement',
181        'requiredLowercase',
182        'requiredNumeric',
183        'requiredSpecial',
184        'requiredUppercase',
185    ]
186
187    returnables = [
188        'expiration_warning',
189        'max_duration',
190        'max_login_failures',
191        'min_duration',
192        'min_length',
193        'password_memory',
194        'policy_enforcement',
195        'required_lowercase',
196        'required_numeric',
197        'required_special',
198        'required_uppercase',
199    ]
200
201    updatables = [
202        'expiration_warning',
203        'max_duration',
204        'max_login_failures',
205        'min_duration',
206        'min_length',
207        'password_memory',
208        'policy_enforcement',
209        'required_lowercase',
210        'required_numeric',
211        'required_special',
212        'required_uppercase',
213    ]
214
215    @property
216    def policy_enforcement(self):
217        return flatten_boolean(self._values['policy_enforcement'])
218
219
220class ApiParameters(Parameters):
221    pass
222
223
224class ModuleParameters(Parameters):
225    pass
226
227
228class Changes(Parameters):
229    def to_return(self):
230        result = {}
231        try:
232            for returnable in self.returnables:
233                result[returnable] = getattr(self, returnable)
234            result = self._filter_params(result)
235        except Exception:
236            pass
237        return result
238
239
240class UsableChanges(Changes):
241    @property
242    def policy_enforcement(self):
243        if self._values['policy_enforcement'] is None:
244            return None
245        if self._values['policy_enforcement'] == 'yes':
246            return 'enabled'
247        return 'disabled'
248
249
250class ReportableChanges(Changes):
251    @property
252    def policy_enforcement(self):
253        return flatten_boolean(self._values['policy_enforcement'])
254
255
256class Difference(object):
257    def __init__(self, want, have=None):
258        self.want = want
259        self.have = have
260
261    def compare(self, param):
262        try:
263            result = getattr(self, param)
264            return result
265        except AttributeError:
266            return self.__default(param)
267
268    def __default(self, param):
269        attr1 = getattr(self.want, param)
270        try:
271            attr2 = getattr(self.have, param)
272            if attr1 != attr2:
273                return attr1
274        except AttributeError:
275            return attr1
276
277
278class ModuleManager(object):
279    def __init__(self, *args, **kwargs):
280        self.module = kwargs.get('module', None)
281        self.client = F5RestClient(**self.module.params)
282        self.want = ModuleParameters(params=self.module.params)
283        self.have = ApiParameters()
284        self.changes = UsableChanges()
285
286    def _set_changed_options(self):
287        changed = {}
288        for key in Parameters.returnables:
289            if getattr(self.want, key) is not None:
290                changed[key] = getattr(self.want, key)
291        if changed:
292            self.changes = UsableChanges(params=changed)
293
294    def _update_changed_options(self):
295        diff = Difference(self.want, self.have)
296        updatables = Parameters.updatables
297        changed = dict()
298        for k in updatables:
299            change = diff.compare(k)
300            if change is None:
301                continue
302            else:
303                if isinstance(change, dict):
304                    changed.update(change)
305                else:
306                    changed[k] = change
307        if changed:
308            self.changes = UsableChanges(params=changed)
309            return True
310        return False
311
312    def _announce_deprecations(self, result):
313        warnings = result.pop('__warnings', [])
314        for warning in warnings:
315            self.client.module.deprecate(
316                msg=warning['msg'],
317                version=warning['version']
318            )
319
320    def exec_module(self):
321        start = datetime.now().isoformat()
322        version = tmos_version(self.client)
323        result = dict()
324
325        changed = self.present()
326
327        reportable = ReportableChanges(params=self.changes.to_return())
328        changes = reportable.to_return()
329        result.update(**changes)
330        result.update(dict(changed=changed))
331        self._announce_deprecations(result)
332        send_teem(start, self.client, self.module, version)
333        return result
334
335    def present(self):
336        return self.update()
337
338    def should_update(self):
339        result = self._update_changed_options()
340        if result:
341            return True
342        return False
343
344    def update(self):
345        self.have = self.read_current_from_device()
346        if not self.should_update():
347            return False
348        if self.module.check_mode:
349            return True
350        self.update_on_device()
351        return True
352
353    def update_on_device(self):
354        params = self.changes.api_params()
355        uri = "https://{0}:{1}/mgmt/tm/auth/password-policy".format(
356            self.client.provider['server'],
357            self.client.provider['server_port'],
358        )
359        resp = self.client.api.patch(uri, json=params)
360        try:
361            response = resp.json()
362        except ValueError as ex:
363            raise F5ModuleError(str(ex))
364
365        if 'code' in response and response['code'] == 400:
366            if 'message' in response:
367                raise F5ModuleError(response['message'])
368            else:
369                raise F5ModuleError(resp.content)
370
371    def read_current_from_device(self):
372        uri = "https://{0}:{1}/mgmt/tm/auth/password-policy".format(
373            self.client.provider['server'],
374            self.client.provider['server_port'],
375        )
376        resp = self.client.api.get(uri)
377        try:
378            response = resp.json()
379        except ValueError as ex:
380            raise F5ModuleError(str(ex))
381
382        if 'code' in response and response['code'] == 400:
383            if 'message' in response:
384                raise F5ModuleError(response['message'])
385            else:
386                raise F5ModuleError(resp.content)
387        return ApiParameters(params=response)
388
389
390class ArgumentSpec(object):
391    def __init__(self):
392        self.supports_check_mode = True
393        argument_spec = dict(
394            expiration_warning=dict(type='int'),
395            max_duration=dict(type='int'),
396            max_login_failures=dict(type='int'),
397            min_duration=dict(type='int'),
398            min_length=dict(type='int'),
399            password_memory=dict(type='int'),
400            policy_enforcement=dict(type='bool'),
401            required_lowercase=dict(type='int'),
402            required_numeric=dict(type='int'),
403            required_special=dict(type='int'),
404            required_uppercase=dict(type='int'),
405        )
406        self.argument_spec = {}
407        self.argument_spec.update(f5_argument_spec)
408        self.argument_spec.update(argument_spec)
409
410
411def main():
412    spec = ArgumentSpec()
413
414    module = AnsibleModule(
415        argument_spec=spec.argument_spec,
416        supports_check_mode=spec.supports_check_mode,
417    )
418
419    try:
420        mm = ModuleManager(module=module)
421        results = mm.exec_module()
422        module.exit_json(**results)
423    except F5ModuleError as ex:
424        module.fail_json(msg=str(ex))
425
426
427if __name__ == '__main__':
428    main()
429