#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright: (c) 2018, F5 Networks Inc. # GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} DOCUMENTATION = r''' --- module: bigip_device_auth short_description: Manage system authentication on a BIG-IP description: - Manage the system authentication configuration. This module can assist in configuring a number of different system authentication types. Note that this module can not be used to configure APM authentication types. version_added: 2.7 options: type: description: - The authentication type to manage with this module. - Take special note that the parameters supported by this module will vary depending on the C(type) that you are configuring. - This module only supports a subset, at this time, of the total available auth types. choices: - tacacs - local servers: description: - Specifies a list of the IPv4 addresses for servers using the Terminal Access Controller Access System (TACACS)+ protocol with which the system communicates to obtain authorization data. - For each address, an alternate TCP port number may be optionally specified by specifying the C(port) key. - If no port number is specified, the default port C(49163) is used. - This parameter is supported by the C(tacacs) type. suboptions: address: description: - The IP address of the server. - This field is required, unless you are specifying a simple list of servers. In that case, the simple list can specify server IPs. See examples for more clarification. port: description: - The port of the server. default: 49163 secret: description: - Secret key used to encrypt and decrypt packets sent or received from the server. - B(Do not) use the pound/hash sign in the secret for TACACS+ servers. - When configuring TACACS+ auth for the first time, this value is required. service_name: description: - Specifies the name of the service that the user is requesting to be authorized to use. - Identifying what the user is asking to be authorized for, enables the TACACS+ server to behave differently for different types of authorization requests. - When configuring this form of system authentication, this setting is required. - Note that the majority of TACACS+ implementations are of service type C(ppp), so try that first. choices: - slip - ppp - arap - shell - tty-daemon - connection - system - firewall protocol_name: description: - Specifies the protocol associated with the value specified in C(service_name), which is a subset of the associated service being used for client authorization or system accounting. - Note that the majority of TACACS+ implementations are of protocol type C(ip), so try that first. choices: - lcp - ip - ipx - atalk - vines - lat - xremote - tn3270 - telnet - rlogin - pad - vpdn - ftp - http - deccp - osicp - unknown authentication: description: - Specifies the process the system employs when sending authentication requests. - When C(use-first-server), specifies that the system sends authentication attempts to only the first server in the list. - When C(use-all-servers), specifies that the system sends an authentication request to each server until authentication succeeds, or until the system has sent a request to all servers in the list. - This parameter is supported by the C(tacacs) type. choices: - use-first-server - use-all-servers use_for_auth: description: - Specifies whether or not this auth source is put in use on the system. type: bool state: description: - The state of the authentication configuration on the system. - When C(present), guarantees that the system is configured for the specified C(type). - When C(absent), sets the system auth source back to C(local). default: present choices: - absent - present update_secret: description: - C(always) will allow to update secrets if the user chooses to do so. - C(on_create) will only set the secret when a C(use_auth_source) is C(yes) and TACACS+ is not currently the auth source. default: always choices: - always - on_create extends_documentation_fragment: f5 author: - Tim Rupp (@caphrim007) ''' EXAMPLES = r''' - name: Set the system auth to TACACS+, default server port bigip_device_auth: type: tacacs authentication: use-all-servers protocol_name: ip secret: secret servers: - 10.10.10.10 - 10.10.10.11 service_name: ppp state: present use_for_auth: yes provider: password: secret server: lb.mydomain.com user: admin delegate_to: localhost - name: Set the system auth to TACACS+, override server port bigip_device_auth: type: tacacs authentication: use-all-servers protocol_name: ip secret: secret servers: - address: 10.10.10.10 port: 1234 - 10.10.10.11 service_name: ppp use_for_auth: yes state: present provider: password: secret server: lb.mydomain.com user: admin delegate_to: localhost ''' RETURN = r''' servers: description: List of servers used in TACACS authentication. returned: changed type: list sample: ['1.2.2.1', '4.5.5.4'] authentication: description: Process the system uses to serve authentication requests when using TACACS. returned: changed type: string sample: use-all-servers service_name: description: Name of the service the user is requesting to be authorized to use. returned: changed type: string sample: ppp protocol_name: description: Name of the protocol associated with C(service_name) used for client authentication. returned: changed type: string sample: ip ''' from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.six import string_types try: from library.module_utils.network.f5.bigip import F5RestClient from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import AnsibleF5Parameters from library.module_utils.network.f5.common import cleanup_tokens from library.module_utils.network.f5.common import fq_name from library.module_utils.network.f5.common import f5_argument_spec from library.module_utils.network.f5.common import exit_json from library.module_utils.network.f5.common import fail_json except ImportError: from ansible.module_utils.network.f5.bigip import F5RestClient from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import AnsibleF5Parameters from ansible.module_utils.network.f5.common import cleanup_tokens from ansible.module_utils.network.f5.common import fq_name from ansible.module_utils.network.f5.common import f5_argument_spec from ansible.module_utils.network.f5.common import exit_json from ansible.module_utils.network.f5.common import fail_json class BaseParameters(AnsibleF5Parameters): @property def api_map(self): return {} @property def api_attributes(self): return [] @property def returnables(self): return [] @property def updatables(self): return [] class BaseApiParameters(BaseParameters): pass class BaseModuleParameters(BaseParameters): pass class BaseChanges(BaseParameters): def to_return(self): result = {} try: for returnable in self.returnables: result[returnable] = getattr(self, returnable) result = self._filter_params(result) except Exception: pass return result class BaseUsableChanges(BaseChanges): pass class BaseReportableChanges(BaseChanges): pass class TacacsParameters(BaseParameters): api_map = { 'protocol': 'protocol_name', 'service': 'service_name' } api_attributes = [ 'authentication', 'protocol', 'service', 'secret', 'servers' ] returnables = [ 'servers', 'secret', 'authentication', 'service_name', 'protocol_name' ] updatables = [ 'servers', 'secret', 'authentication', 'service_name', 'protocol_name', 'auth_source', ] class TacacsApiParameters(TacacsParameters): pass class TacacsModuleParameters(TacacsParameters): @property def servers(self): if self._values['servers'] is None: return None result = [] for server in self._values['servers']: if isinstance(server, dict): if 'address' not in server: raise F5ModuleError( "An 'address' field must be provided when specifying separate fields to the 'servers' parameter." ) address = server.get('address') port = server.get('port', 49163) elif isinstance(server, string_types): address = server port = 49163 result.append('{0}:{1}'.format(address, port)) return result @property def auth_source(self): return 'tacacs' class TacacsChanges(BaseChanges, TacacsParameters): pass class TacacsUsableChanges(TacacsChanges): pass class TacacsReportableChanges(TacacsChanges): @property def secret(self): return None class Difference(object): def __init__(self, want, have=None): self.want = want self.have = have def compare(self, param): try: result = getattr(self, param) return result except AttributeError: return self.__default(param) def __default(self, param): want = getattr(self.want, param) try: have = getattr(self.have, param) if want != have: return want except AttributeError: return want @property def secret(self): if self.want.secret != self.have.secret and self.want.update_secret == 'always': return self.want.secret class BaseManager(object): def _set_changed_options(self): changed = {} for key in self.returnables: if getattr(self.want, key) is not None: changed[key] = getattr(self.want, key) if changed: self.changes = self.get_usable_changes(params=changed) def _update_changed_options(self): diff = Difference(self.want, self.have) updatables = self.updatables changed = dict() for k in updatables: change = diff.compare(k) if change is None: continue else: if isinstance(change, dict): changed.update(change) else: changed[k] = change if changed: self.changes = self.get_usable_changes(params=changed) return True return False def should_update(self): result = self._update_changed_options() if result: return True return False def exec_module(self): changed = False result = dict() state = self.want.state if state == "present": changed = self.present() elif state == "absent": changed = self.absent() reportable = self.get_reportable_changes(params=self.changes.to_return()) changes = reportable.to_return() result.update(**changes) result.update(dict(changed=changed)) self._announce_deprecations(result) return result def _announce_deprecations(self, result): warnings = result.pop('__warnings', []) for warning in warnings: self.client.module.deprecate( msg=warning['msg'], version=warning['version'] ) def present(self): if self.exists(): return self.update() return self.create() def absent(self): if self.exists(): return self.remove() return False def update_auth_source_on_device(self, source): """Set the system auth source. Configuring the authentication source is only one step in the process of setting up an auth source. The other step is to inform the system of the auth source you want to use. This method is used for situations where * The ``use_for_auth`` parameter is set to ``yes`` * The ``use_for_auth`` parameter is set to ``no`` * The ``state`` parameter is set to ``absent`` When ``state`` equal to ``absent``, before you can delete the TACACS+ configuration, you must set the system auth to "something else". The system ships with a system auth called "local", so this is the logical "something else" to use. When ``use_for_auth`` is no, the same situation applies as when ``state`` equal to ``absent`` is done above. When ``use_for_auth`` is ``yes``, this method will set the current system auth state to TACACS+. Arguments: source (string): The source that you want to set on the device. """ params = dict( type=source ) uri = 'https://{0}:{1}/mgmt/tm/auth/source/'.format( self.client.provider['server'], self.client.provider['server_port'] ) resp = self.client.api.patch(uri, json=params) try: response = resp.json() except ValueError as ex: raise F5ModuleError(str(ex)) if 'code' in response and response['code'] == 400: if 'message' in response: raise F5ModuleError(response['message']) else: raise F5ModuleError(resp.content) def read_current_auth_source_from_device(self): uri = "https://{0}:{1}/mgmt/tm/auth/source".format( self.client.provider['server'], self.client.provider['server_port'], ) resp = self.client.api.get(uri) try: response = resp.json() except ValueError as ex: raise F5ModuleError(str(ex)) if 'code' in response and response['code'] == 400: if 'message' in response: raise F5ModuleError(response['message']) else: raise F5ModuleError(resp.content) return response['type'] class LocalManager(BaseManager): def __init__(self, *args, **kwargs): self.module = kwargs.get('module', None) self.client = kwargs.get('client', None) self.want = self.get_module_parameters(params=self.module.params) self.have = self.get_api_parameters() self.changes = self.get_usable_changes() @property def returnables(self): return [] @property def updatables(self): return [] def get_parameters(self, params=None): return BaseParameters(params=params) def get_usable_changes(self, params=None): return BaseUsableChanges(params=params) def get_reportable_changes(self, params=None): return BaseReportableChanges(params=params) def get_module_parameters(self, params=None): return BaseModuleParameters(params=params) def get_api_parameters(self, params=None): return BaseApiParameters(params=params) def exists(self): uri = "https://{0}:{1}/mgmt/tm/auth/source".format( self.client.provider['server'], self.client.provider['server_port'], ) resp = self.client.api.get(uri) try: response = resp.json() except ValueError as ex: raise F5ModuleError(str(ex)) if 'code' in response and response['code'] == 400: if 'message' in response: raise F5ModuleError(response['message']) else: raise F5ModuleError(resp.content) if response['type'] == 'local': return True return False def create(self): self._set_changed_options() if self.module.check_mode: return True self.update_auth_source_on_device('local') return True def present(self): if not self.exists(): return self.create() def absent(self): raise F5ModuleError( "The 'local' type cannot be removed. " "Instead, specify a 'state' of 'present' on other types." ) class TacacsManager(BaseManager): def __init__(self, *args, **kwargs): self.module = kwargs.get('module', None) self.client = kwargs.get('client', None) self.want = self.get_module_parameters(params=self.module.params) self.have = self.get_api_parameters() self.changes = self.get_usable_changes() @property def returnables(self): return TacacsParameters.returnables @property def updatables(self): return TacacsParameters.updatables def get_usable_changes(self, params=None): return TacacsUsableChanges(params=params) def get_reportable_changes(self, params=None): return TacacsReportableChanges(params=params) def get_module_parameters(self, params=None): return TacacsModuleParameters(params=params) def get_api_parameters(self, params=None): return TacacsApiParameters(params=params) def exists(self): uri = "https://{0}:{1}/mgmt/tm/auth/tacacs/~Common~system-auth".format( self.client.provider['server'], self.client.provider['server_port'], ) resp = self.client.api.get(uri) try: response = resp.json() except ValueError: return False if resp.status == 404 or 'code' in response and response['code'] == 404: return False return True def create(self): self._set_changed_options() if self.module.check_mode: return True self.create_on_device() if self.want.use_for_auth: self.update_auth_source_on_device('tacacs') return True def update(self): self.have = self.read_current_from_device() if not self.should_update(): return False if self.module.check_mode: return True result = False if self.update_on_device(): result = True if self.want.use_for_auth and self.changes.auth_source == 'tacacs': self.update_auth_source_on_device('tacacs') result = True return result def remove(self): if self.module.check_mode: return True self.update_auth_source_on_device('local') self.remove_from_device() if self.exists(): raise F5ModuleError("Failed to delete the resource.") return True def create_on_device(self): params = self.changes.api_params() params['name'] = 'system-auth' uri = "https://{0}:{1}/mgmt/tm/auth/tacacs".format( self.client.provider['server'], self.client.provider['server_port'] ) resp = self.client.api.post(uri, json=params) try: response = resp.json() except ValueError as ex: raise F5ModuleError(str(ex)) if 'code' in response and response['code'] in [400, 403]: if 'message' in response: raise F5ModuleError(response['message']) else: raise F5ModuleError(resp.content) def update_on_device(self): params = self.changes.api_params() if not params: return False uri = 'https://{0}:{1}/mgmt/tm/auth/tacacs/~Common~system-auth'.format( self.client.provider['server'], self.client.provider['server_port'] ) resp = self.client.api.patch(uri, json=params) try: response = resp.json() except ValueError as ex: raise F5ModuleError(str(ex)) if 'code' in response and response['code'] == 400: if 'message' in response: raise F5ModuleError(response['message']) else: raise F5ModuleError(resp.content) return True def remove_from_device(self): uri = "https://{0}:{1}/mgmt/tm/auth/tacacs/~Common~system-auth".format( self.client.provider['server'], self.client.provider['server_port'] ) resp = self.client.api.delete(uri) if resp.status == 200: return True raise F5ModuleError(resp.content) def read_current_from_device(self): uri = "https://{0}:{1}/mgmt/tm/auth/tacacs/~Common~system-auth".format( self.client.provider['server'], self.client.provider['server_port'], ) resp = self.client.api.get(uri) try: response = resp.json() except ValueError as ex: raise F5ModuleError(str(ex)) if 'code' in response and response['code'] == 400: if 'message' in response: raise F5ModuleError(response['message']) else: raise F5ModuleError(resp.content) response['auth_source'] = self.read_current_auth_source_from_device() return self.get_api_parameters(params=response) class ModuleManager(object): def __init__(self, *args, **kwargs): self.module = kwargs.get('module', None) self.client = kwargs.get('client', None) self.kwargs = kwargs def exec_module(self): manager = self.get_manager(self.module.params['type']) return manager.exec_module() def get_manager(self, type): if type == 'tacacs': return TacacsManager(**self.kwargs) elif type == 'local': return LocalManager(**self.kwargs) else: raise F5ModuleError( "The provided 'type' is unknown." ) class ArgumentSpec(object): def __init__(self): self.supports_check_mode = True argument_spec = dict( type=dict( required=True, choices=['local', 'tacacs'] ), servers=dict(type='raw'), secret=dict(no_log=True), service_name=dict( choices=[ 'slip', 'ppp', 'arap', 'shell', 'tty-daemon', 'connection', 'system', 'firewall' ] ), protocol_name=dict( choices=[ 'lcp', 'ip', 'ipx', 'atalk', 'vines', 'lat', 'xremote', 'tn3270', 'telnet', 'rlogin', 'pad', 'vpdn', 'ftp', 'http', 'deccp', 'osicp', 'unknown' ] ), authentication=dict( choices=[ 'use-first-server', 'use-all-servers' ] ), use_for_auth=dict(type='bool'), update_secret=dict( choices=['always', 'on_create'], default='always' ), state=dict( default='present', choices=['present', 'absent'] ), ) self.argument_spec = {} self.argument_spec.update(f5_argument_spec) self.argument_spec.update(argument_spec) def main(): spec = ArgumentSpec() module = AnsibleModule( argument_spec=spec.argument_spec, supports_check_mode=spec.supports_check_mode, ) try: client = F5RestClient(**module.params) mm = ModuleManager(module=module, client=client) results = mm.exec_module() exit_json(module, results, client) except F5ModuleError as ex: fail_json(module, ex, client) if __name__ == '__main__': main()