Adds module to configure ldap device auth on a bigip (#48974)

This commit is contained in:
Tim Rupp 2018-11-20 22:15:50 -08:00 committed by GitHub
parent 44b20fbf96
commit 212b48b6fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 910 additions and 0 deletions

View file

@ -0,0 +1,775 @@
#!/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': 'certified'}
DOCUMENTATION = r'''
---
module: bigip_device_auth_ldap
short_description: Manage LDAP device authentication settings on BIG-IP
description:
- Manage LDAP device authentication settings on BIG-IP.
version_added: 2.8
options:
servers:
description:
- Specifies the LDAP servers that the system must use to obtain
authentication information. You must specify a server when you
create an LDAP configuration object.
port:
description:
- Specifies the port that the system uses for access to the remote host server.
- When configuring LDAP device authentication for the first time, if this parameter
is not specified, the default port is C(389).
remote_directory_tree:
description:
- Specifies the file location (tree) of the user authentication database on the
server.
scope:
description:
- Specifies the level of the remote Active Directory or LDAP directory that the
system should search for the user authentication.
choices:
- sub
- one
- base
bind_dn:
description:
- Specifies the distinguished name for the Active Directory or LDAP server user
ID.
- The BIG-IP client authentication module does not support Active Directory or
LDAP servers that do not perform bind referral when authenticating referred
accounts.
- Therefore, if you plan to use Active Directory or LDAP as your authentication
source and want to use referred accounts, make sure your servers perform bind
referral.
bind_password:
description:
- Specifies a password for the Active Directory or LDAP server user ID.
user_template:
description:
- Specifies the distinguished name of the user who is logging on.
- You specify the template as a variable that the system replaces with user-specific
information during the logon attempt.
- For example, you could specify a user template such as C(%s@siterequest.com) or
C(uxml:id=%s,ou=people,dc=siterequest,dc=com).
- When a user attempts to log on, the system replaces C(%s) with the name the user
specified in the Basic Authentication dialog box, and passes that as the
distinguished name for the bind operation.
- The system passes the associated password as the password for the bind operation.
- This field can contain only one C(%s) and cannot contain any other format
specifiers.
check_member_attr:
description:
- Checks the user's member attribute in the remote LDAP or AD group.
type: bool
ssl:
description:
- Specifies whether the system uses an SSL port to communicate with the LDAP server.
choices:
- "yes"
- "no"
- start-tls
ssl_ca_cert:
description:
- Specifies the name of an SSL certificate from a certificate authority (CA).
- To remove this value, use the reserved value C(none).
ssl_client_key:
description:
- Specifies the name of an SSL client key.
- To remove this value, use the reserved value C(none).
ssl_client_cert:
description:
- Specifies the name of an SSL client certificate.
- To remove this value, use the reserved value C(none).
ssl_check_peer:
description:
- Specifies whether the system checks an SSL peer, as a result of which the
system requires and verifies the server certificate.
type: bool
login_ldap_attr:
description:
- Specifies the LDAP directory attribute containing the local user name that is
associated with the selected directory entry.
- When configuring LDAP device authentication for the first time, if this parameter
is not specified, the default port is C(samaccountname).
fallback_to_local:
description:
- Specifies that the system uses the Local authentication method if the remote
authentication method is not available.
type: bool
state:
description:
- When C(present), ensures the device authentication method exists.
- When C(absent), ensures the device authentication method does not exist.
default: present
choices:
- present
- absent
update_password:
description:
- C(always) will always update the C(bind_password).
- C(on_create) will only set the C(bind_password) for newly created authentication
mechanisms.
default: always
choices:
- always
- on_create
extends_documentation_fragment: f5
author:
- Tim Rupp (@caphrim007)
'''
EXAMPLES = r'''
- name: Create an LDAP authentication object
bigip_device_auth_ldap:
name: foo
provider:
password: secret
server: lb.mydomain.com
user: admin
delegate_to: localhost
'''
RETURN = r'''
param1:
description: The new param1 value of the resource.
returned: changed
type: bool
sample: true
param2:
description: The new param2 value of the resource.
returned: changed
type: string
sample: Foo is bar
'''
from ansible.module_utils.basic import AnsibleModule
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 transform_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
from library.module_utils.network.f5.common import flatten_boolean
from library.module_utils.network.f5.compare import cmp_str_with_none
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 transform_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
from ansible.module_utils.network.f5.common import flatten_boolean
from ansible.module_utils.network.f5.compare import cmp_str_with_none
class Parameters(AnsibleF5Parameters):
api_map = {
'bindDn': 'bind_dn',
'bindPw': 'bind_password',
'userTemplate': 'user_template',
'fallback': 'fallback_to_local',
'loginAttribute': 'login_ldap_attr',
'sslCheckPeer': 'ssl_check_peer',
'sslClientCert': 'ssl_client_cert',
'sslClientKey': 'ssl_client_key',
'sslCaCertFile': 'ssl_ca_cert',
'checkRolesGroup': 'check_member_attr',
'searchBaseDn': 'remote_directory_tree',
}
api_attributes = [
'bindDn',
'bindPw',
'checkRolesGroup',
'loginAttribute',
'port',
'scope',
'searchBaseDn',
'servers',
'ssl',
'sslCaCertFile',
'sslCheckPeer',
'sslClientCert',
'sslClientKey',
'userTemplate',
]
returnables = [
'bind_dn',
'bind_password',
'check_member_attr',
'fallback_to_local',
'login_ldap_attr',
'port',
'remote_directory_tree',
'scope',
'servers',
'ssl',
'ssl_ca_cert',
'ssl_check_peer',
'ssl_client_cert',
'ssl_client_key',
'user_template',
]
updatables = [
'bind_dn',
'bind_password',
'check_member_attr',
'fallback_to_local',
'login_ldap_attr',
'port',
'remote_directory_tree',
'scope',
'servers',
'ssl',
'ssl_ca_cert',
'ssl_check_peer',
'ssl_client_cert',
'ssl_client_key',
'user_template',
]
@property
def ssl_ca_cert(self):
if self._values['ssl_ca_cert'] is None:
return None
elif self._values['ssl_ca_cert'] in ['none', '']:
return ''
return fq_name(self.partition, self._values['ssl_ca_cert'])
@property
def ssl_client_key(self):
if self._values['ssl_client_key'] is None:
return None
elif self._values['ssl_client_key'] in ['none', '']:
return ''
return fq_name(self.partition, self._values['ssl_client_key'])
@property
def ssl_client_cert(self):
if self._values['ssl_client_cert'] is None:
return None
elif self._values['ssl_client_cert'] in ['none', '']:
return ''
return fq_name(self.partition, self._values['ssl_client_cert'])
@property
def ssl_check_peer(self):
return flatten_boolean(self._values['ssl_check_peer'])
@property
def fallback_to_local(self):
return flatten_boolean(self._values['fallback_to_local'])
@property
def check_member_attr(self):
return flatten_boolean(self._values['check_member_attr'])
@property
def login_ldap_attr(self):
if self._values['login_ldap_attr'] is None:
return None
elif self._values['login_ldap_attr'] in ['none', '']:
return ''
return self._values['login_ldap_attr']
@property
def user_template(self):
if self._values['user_template'] is None:
return None
elif self._values['user_template'] in ['none', '']:
return ''
return self._values['user_template']
@property
def ssl(self):
if self._values['ssl'] is None:
return None
elif self._values['ssl'] == 'start-tls':
return 'start-tls'
return flatten_boolean(self._values['ssl'])
class ApiParameters(Parameters):
pass
class ModuleParameters(Parameters):
pass
class Changes(Parameters):
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 UsableChanges(Changes):
@property
def ssl_check_peer(self):
if self._values['ssl_check_peer'] is None:
return None
elif self._values['ssl_check_peer'] == 'yes':
return 'enabled'
return 'disabled'
@property
def fallback_to_local(self):
if self._values['fallback_to_local'] is None:
return None
elif self._values['fallback_to_local'] == 'yes':
return 'true'
return 'false'
@property
def check_member_attr(self):
if self._values['check_member_attr'] is None:
return None
elif self._values['check_member_attr'] == 'yes':
return 'enabled'
return 'disabled'
@property
def ssl(self):
if self._values['ssl'] is None:
return None
elif self._values['ssl'] == 'start-tls':
return 'start-tls'
elif self._values['ssl'] == 'yes':
return 'enabled'
return 'disabled'
class ReportableChanges(Changes):
@property
def bind_password(self):
return None
@property
def ssl_check_peer(self):
return flatten_boolean(self._values['ssl_check_peer'])
@property
def check_member_attr(self):
return flatten_boolean(self._values['check_member_attr'])
@property
def ssl(self):
if self._values['ssl'] is None:
return None
elif self._values['ssl'] == 'start-tls':
return 'start-tls'
return flatten_boolean(self._values['ssl'])
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):
attr1 = getattr(self.want, param)
try:
attr2 = getattr(self.have, param)
if attr1 != attr2:
return attr1
except AttributeError:
return attr1
@property
def login_ldap_attr(self):
return cmp_str_with_none(self.want.login_ldap_attr, self.have.login_ldap_attr)
@property
def user_template(self):
return cmp_str_with_none(self.want.user_template, self.have.user_template)
@property
def ssl_ca_cert(self):
return cmp_str_with_none(self.want.ssl_ca_cert, self.have.ssl_ca_cert)
@property
def ssl_client_key(self):
return cmp_str_with_none(self.want.ssl_client_key, self.have.ssl_client_key)
@property
def ssl_client_cert(self):
return cmp_str_with_none(self.want.ssl_client_cert, self.have.ssl_client_cert)
@property
def bind_password(self):
if self.want.bind_password != self.have.bind_password and self.want.update_password == 'always':
return self.want.bind_password
class ModuleManager(object):
def __init__(self, *args, **kwargs):
self.module = kwargs.get('module', None)
self.client = kwargs.get('client', None)
self.want = ModuleParameters(params=self.module.params)
self.have = ApiParameters()
self.changes = UsableChanges()
def _set_changed_options(self):
changed = {}
for key in Parameters.returnables:
if getattr(self.want, key) is not None:
changed[key] = getattr(self.want, key)
if changed:
self.changes = UsableChanges(params=changed)
def _update_changed_options(self):
diff = Difference(self.want, self.have)
updatables = Parameters.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 = UsableChanges(params=changed)
return True
return False
def _announce_deprecations(self, result):
warnings = result.pop('__warnings', [])
for warning in warnings:
self.client.module.deprecate(
msg=warning['msg'],
version=warning['version']
)
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 update_fallback_on_device(self, fallback):
params = dict(
fallback=fallback
)
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 exec_module(self):
changed = False
result = dict()
state = self.want.state
if state == "present":
changed = self.present()
elif state == "absent":
changed = self.absent()
reportable = ReportableChanges(params=self.changes.to_return())
changes = reportable.to_return()
result.update(**changes)
result.update(dict(changed=changed))
self._announce_deprecations(result)
return result
def present(self):
if self.exists():
return self.update()
else:
return self.create()
def absent(self):
if self.exists():
return self.remove()
return False
def should_update(self):
result = self._update_changed_options()
if result:
return True
return False
def update(self):
self.have = self.read_current_from_device()
if not self.should_update():
return False
if self.module.check_mode:
return True
self.update_on_device()
if self.want.fallback_to_local == 'yes':
self.update_fallback_on_device('true')
elif self.want.fallback_to_local == 'no':
self.update_fallback_on_device('false')
return True
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(self):
self._set_changed_options()
if self.module.check_mode:
return True
self.create_on_device()
if self.want.fallback_to_local == 'yes':
self.update_fallback_on_device('true')
elif self.want.fallback_to_local == 'no':
self.update_fallback_on_device('false')
return True
def exists(self):
uri = "https://{0}:{1}/mgmt/tm/auth/ldap/{2}".format(
self.client.provider['server'],
self.client.provider['server_port'],
transform_name('Common', 'system-auth')
)
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_on_device(self):
params = self.changes.api_params()
params['name'] = 'system-auth'
params['partition'] = 'Common'
uri = "https://{0}:{1}/mgmt/tm/auth/ldap/".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, 409]:
if 'message' in response:
raise F5ModuleError(response['message'])
else:
raise F5ModuleError(resp.content)
return True
def update_on_device(self):
params = self.changes.api_params()
if not params:
return
uri = "https://{0}:{1}/mgmt/tm/auth/ldap/{2}".format(
self.client.provider['server'],
self.client.provider['server_port'],
transform_name('Common', 'system-auth')
)
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 remove_from_device(self):
uri = "https://{0}:{1}/mgmt/tm/auth/ldap/{2}".format(
self.client.provider['server'],
self.client.provider['server_port'],
transform_name('Common', 'system-auth')
)
response = self.client.api.delete(uri)
if response.status == 200:
return True
raise F5ModuleError(response.content)
def read_current_from_device(self):
uri = "https://{0}:{1}/mgmt/tm/auth/ldap/{2}".format(
self.client.provider['server'],
self.client.provider['server_port'],
transform_name('Common', 'system-auth')
)
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)
result = ApiParameters(params=response)
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)
result.update({'fallback': response['fallback']})
return result
class ArgumentSpec(object):
def __init__(self):
self.supports_check_mode = True
argument_spec = dict(
servers=dict(type='list'),
port=dict(type='int'),
remote_directory_tree=dict(),
scope=dict(
choices=['sub', 'one', 'base']
),
bind_dn=dict(),
bind_password=dict(no_log=True),
user_template=dict(),
check_member_attr=dict(type='bool'),
ssl=dict(
choices=['yes', 'no', 'start-tls']
),
ssl_ca_cert=dict(),
ssl_client_key=dict(),
ssl_client_cert=dict(),
ssl_check_peer=dict(type='bool'),
login_ldap_attr=dict(),
fallback_to_local=dict(type='bool'),
update_password=dict(
default='always',
choices=['always', 'on_create']
),
state=dict(default='present', choices=['absent', 'present']),
)
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,
)
client = F5RestClient(**module.params)
try:
mm = ModuleManager(module=module, client=client)
results = mm.exec_module()
cleanup_tokens(client)
exit_json(module, results, client)
except F5ModuleError as ex:
cleanup_tokens(client)
fail_json(module, ex, client)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,135 @@
# -*- 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
import os
import json
import pytest
import sys
if sys.version_info < (2, 7):
pytestmark = pytest.mark.skip("F5 Ansible modules require Python >= 2.7")
from ansible.module_utils.basic import AnsibleModule
try:
from library.modules.bigip_device_auth_ldap import ApiParameters
from library.modules.bigip_device_auth_ldap import ModuleParameters
from library.modules.bigip_device_auth_ldap import ModuleManager
from library.modules.bigip_device_auth_ldap import ArgumentSpec
# In Ansible 2.8, Ansible changed import paths.
from test.units.compat import unittest
from test.units.compat.mock import Mock
from test.units.compat.mock import patch
from test.units.modules.utils import set_module_args
except ImportError:
from ansible.modules.network.f5.bigip_device_auth_ldap import ApiParameters
from ansible.modules.network.f5.bigip_device_auth_ldap import ModuleParameters
from ansible.modules.network.f5.bigip_device_auth_ldap import ModuleManager
from ansible.modules.network.f5.bigip_device_auth_ldap import ArgumentSpec
# Ansible 2.8 imports
from units.compat import unittest
from units.compat.mock import Mock
from units.compat.mock import patch
from units.modules.utils import set_module_args
fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures')
fixture_data = {}
def load_fixture(name):
path = os.path.join(fixture_path, name)
if path in fixture_data:
return fixture_data[path]
with open(path) as f:
data = f.read()
try:
data = json.loads(data)
except Exception:
pass
fixture_data[path] = data
return data
class TestParameters(unittest.TestCase):
def test_module_parameters(self):
args = dict(
servers=['10.10.10.10', '10.10.10.11'],
port=389,
remote_directory_tree='foo',
scope='base',
bind_dn='bar',
bind_password='secret',
user_template='alice',
check_member_attr=False,
ssl='no',
ssl_ca_cert='default.crt',
ssl_client_key='default.key',
ssl_client_cert='default1.crt',
ssl_check_peer=True,
login_ldap_attr='bob',
fallback_to_local=True,
update_password='on_create',
)
p = ApiParameters(params=args)
assert p.port == 389
assert p.servers == ['10.10.10.10', '10.10.10.11']
assert p.remote_directory_tree == 'foo'
assert p.scope == 'base'
assert p.bind_dn == 'bar'
assert p.bind_password == 'secret'
assert p.user_template == 'alice'
assert p.check_member_attr == 'no'
assert p.ssl == 'no'
assert p.ssl_ca_cert == '/Common/default.crt'
assert p.ssl_client_key == '/Common/default.key'
assert p.ssl_client_cert == '/Common/default1.crt'
assert p.ssl_check_peer == 'yes'
assert p.login_ldap_attr == 'bob'
assert p.fallback_to_local == 'yes'
assert p.update_password == 'on_create'
class TestManager(unittest.TestCase):
def setUp(self):
self.spec = ArgumentSpec()
def test_create(self, *args):
set_module_args(dict(
servers=['10.10.10.10', '10.10.10.11'],
update_password='on_create',
state='present',
provider=dict(
password='admin',
server='localhost',
user='admin'
)
))
module = AnsibleModule(
argument_spec=self.spec.argument_spec,
supports_check_mode=self.spec.supports_check_mode
)
# Override methods to force specific logic in the module to happen
mm = ModuleManager(module=module)
mm.exists = Mock(return_value=False)
mm.create_on_device = Mock(return_value=True)
mm.update_auth_source_on_device = Mock(return_value=True)
results = mm.exec_module()
assert results['changed'] is True