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