From 2ed97aeb0cc0d04b5c42e7f7722584bc8ef446d2 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Tue, 9 Jan 2018 10:20:24 -0500 Subject: [PATCH] implement multiple ldap servers --- awx/main/tests/data/ldap_ansible.ldif | 163 ++++++ awx/main/tests/data/ldap_example.ldif | 78 +++ awx/main/tests/data/ldap_redhat.ldif | 163 ++++++ awx/main/tests/functional/conftest.py | 3 +- awx/main/tests/functional/test_ldap.py | 130 +++++ .../tests/functional/utils/test_common.py | 2 +- awx/settings/defaults.py | 5 + awx/sso/backends.py | 20 + awx/sso/conf.py | 514 +++++++++--------- awx/sso/fields.py | 20 + requirements/requirements_dev.txt | 1 + 11 files changed, 846 insertions(+), 253 deletions(-) create mode 100644 awx/main/tests/data/ldap_ansible.ldif create mode 100644 awx/main/tests/data/ldap_example.ldif create mode 100644 awx/main/tests/data/ldap_redhat.ldif create mode 100644 awx/main/tests/functional/test_ldap.py diff --git a/awx/main/tests/data/ldap_ansible.ldif b/awx/main/tests/data/ldap_ansible.ldif new file mode 100644 index 0000000000..f9b8580c54 --- /dev/null +++ b/awx/main/tests/data/ldap_ansible.ldif @@ -0,0 +1,163 @@ + +dn: dc=ansible,dc=com +dc: ansible +description: My wonderful company as much text as you want to place + in this line up to 32K continuation data for the line above must + have <CR> or <CR><LF> i.e. ENTER work + on both Windows and *nix system - new line MUST begin with ONE SPACE +objectClass: dcObject +objectClass: organization +o: ansible.com + +# groups + +dn: ou=groups,dc=ansible,dc=com +objectClass: top +objectClass: organizationalUnit +ou: groups + +# group: Superusers + +dn: cn=superusers,ou=groups,dc=ansible,dc=com +objectClass: top +objectClass: groupOfNames +cn: superusers +member: cn=super_user1,ou=people,dc=ansible,dc=com + +# group: Engineering + +dn: cn=engineering,ou=groups,dc=ansible,dc=com +objectClass: top +objectClass: groupOfNames +cn: engineering +member: cn=eng_admin1,ou=people,dc=ansible,dc=com +member: cn=eng_user1,ou=people,dc=ansible,dc=com +member: cn=eng_user2,ou=people,dc=ansible,dc=com + +dn: cn=engineering_admins,ou=groups,dc=ansible,dc=com +objectClass: top +objectClass: groupOfNames +cn: engineering_admins +member: cn=eng_admin1,ou=people,dc=ansible,dc=com + +# group: Sales + +dn: cn=sales,ou=groups,dc=ansible,dc=com +objectClass: top +objectClass: groupOfNames +cn: sales +member: cn=sales_user1,ou=people,dc=ansible,dc=com +member: cn=sales_user2,ou=people,dc=ansible,dc=com + +# group: IT + +dn: cn=it,ou=groups,dc=ansible,dc=com +objectClass: top +objectClass: groupOfNames +cn: it +member: cn=it_user1,ou=people,dc=ansible,dc=com +member: cn=it_user2,ou=people,dc=ansible,dc=com + + +# users + +dn: ou=people,dc=ansible,dc=com +objectClass: top +objectClass: organizationalUnit +ou: people + +# users - superusers + +dn: cn=super_user1,ou=people,dc=ansible,dc=com +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: super_user1 +sn: User 1 +givenName: Super +mail: super_user1@ansible.com +userPassword: password + +# users - engineering + +dn: cn=eng_user1,ou=people,dc=ansible,dc=com +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: eng_user1 +sn: User 1 +givenName: Engineering +mail: eng_user1@ansible.com +userPassword: password + +dn: cn=eng_user2,ou=people,dc=ansible,dc=com +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: eng_user2 +sn: User 2 +givenName: Engineering +mail: eng_user2@ansible.com +userPassword: password + +dn: cn=eng_admin1,ou=people,dc=ansible,dc=com +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: eng_admin1 +sn: Admin 1 +givenName: Engineering +mail: eng_admin1@ansible.com +userPassword: password + +# users - IT + +dn: cn=it_user1,ou=people,dc=ansible,dc=com +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: it_user1 +sn: Technology User 1 +givenName: Information +mail: it_user1@ansible.com +userPassword: password + +dn: cn=it_user2,ou=people,dc=ansible,dc=com +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: it_user2 +sn: Technology User 2 +givenName: Information +mail: it_user2@ansible.com +userPassword: password + +# users - Sales + +dn: cn=sales_user1,ou=people,dc=ansible,dc=com +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: sales_user1 +sn: Person 1 +givenName: Sales +mail: sales_user1@ansible.com +userPassword: password + +dn: cn=sales_user2,ou=people,dc=ansible,dc=com +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: sales_user2 +sn: Person 2 +givenName: Sales +mail: sales_user2@ansible.com +userPassword: password diff --git a/awx/main/tests/data/ldap_example.ldif b/awx/main/tests/data/ldap_example.ldif new file mode 100644 index 0000000000..b5e53bdced --- /dev/null +++ b/awx/main/tests/data/ldap_example.ldif @@ -0,0 +1,78 @@ + +dn: dc=example,dc=com +dc: example +description: My wonderful company as much text as you want to place + in this line up to 32K continuation data for the line above must + have <CR> or <CR><LF> i.e. ENTER work + on both Windows and *nix system - new line MUST begin with ONE SPACE +objectClass: dcObject +objectClass: organization +o: example.com + +# groups + +dn: ou=groups,dc=example,dc=com +objectClass: top +objectClass: organizationalUnit +ou: groups + +# group: Superusers + +dn: cn=superusers,ou=groups,dc=example,dc=com +objectClass: top +objectClass: groupOfNames +cn: superusers +member: cn=super_user1,ou=people,dc=example,dc=com + +# group: Sales + +dn: cn=sales,ou=groups,dc=example,dc=com +objectClass: top +objectClass: groupOfNames +cn: sales +member: cn=sales_user1,ou=people,dc=example,dc=com +member: cn=sales_user2,ou=people,dc=example,dc=com + +# users + +dn: ou=people,dc=example,dc=com +objectClass: top +objectClass: organizationalUnit +ou: people + +# users - superusers + +dn: cn=super_user1,ou=people,dc=example,dc=com +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: super_user1 +sn: User 1 +givenName: Super +mail: super_user1@example.com +userPassword: password + +# users - Sales + +dn: cn=sales_user1,ou=people,dc=example,dc=com +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: sales_user1 +sn: Person 1 +givenName: Sales +mail: sales_user1@example.com +userPassword: password + +dn: cn=sales_user2,ou=people,dc=example,dc=com +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: sales_user2 +sn: Person 2 +givenName: Sales +mail: sales_user2@example.com +userPassword: password diff --git a/awx/main/tests/data/ldap_redhat.ldif b/awx/main/tests/data/ldap_redhat.ldif new file mode 100644 index 0000000000..82bf1ca78b --- /dev/null +++ b/awx/main/tests/data/ldap_redhat.ldif @@ -0,0 +1,163 @@ + +dn: dc=redhat,dc=com +dc: redhat +description: My wonderful company as much text as you want to place + in this line up to 32K continuation data for the line above must + have <CR> or <CR><LF> i.e. ENTER work + on both Windows and *nix system - new line MUST begin with ONE SPACE +objectClass: dcObject +objectClass: organization +o: redhat.com + +# groups + +dn: ou=groups,dc=redhat,dc=com +objectClass: top +objectClass: organizationalUnit +ou: groups + +# group: Superusers + +dn: cn=superusers,ou=groups,dc=redhat,dc=com +objectClass: top +objectClass: groupOfNames +cn: superusers +member: cn=super_user1,ou=people,dc=redhat,dc=com + +# group: Engineering + +dn: cn=engineering,ou=groups,dc=redhat,dc=com +objectClass: top +objectClass: groupOfNames +cn: engineering +member: cn=eng_admin1,ou=people,dc=redhat,dc=com +member: cn=eng_user1,ou=people,dc=redhat,dc=com +member: cn=eng_user2,ou=people,dc=redhat,dc=com + +dn: cn=engineering_admins,ou=groups,dc=redhat,dc=com +objectClass: top +objectClass: groupOfNames +cn: engineering_admins +member: cn=eng_admin1,ou=people,dc=redhat,dc=com + +# group: Sales + +dn: cn=sales,ou=groups,dc=redhat,dc=com +objectClass: top +objectClass: groupOfNames +cn: sales +member: cn=sales_user1,ou=people,dc=redhat,dc=com +member: cn=sales_user2,ou=people,dc=redhat,dc=com + +# group: IT + +dn: cn=it,ou=groups,dc=redhat,dc=com +objectClass: top +objectClass: groupOfNames +cn: it +member: cn=it_user1,ou=people,dc=redhat,dc=com +member: cn=it_user2,ou=people,dc=redhat,dc=com + + +# users + +dn: ou=people,dc=redhat,dc=com +objectClass: top +objectClass: organizationalUnit +ou: people + +# users - superusers + +dn: cn=super_user1,ou=people,dc=redhat,dc=com +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: super_user1 +sn: User 1 +givenName: Super +mail: super_user1@redhat.com +userPassword: password + +# users - engineering + +dn: cn=eng_user1,ou=people,dc=redhat,dc=com +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: eng_user1 +sn: User 1 +givenName: Engineering +mail: eng_user1@redhat.com +userPassword: password + +dn: cn=eng_user2,ou=people,dc=redhat,dc=com +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: eng_user2 +sn: User 2 +givenName: Engineering +mail: eng_user2@redhat.com +userPassword: password + +dn: cn=eng_admin1,ou=people,dc=redhat,dc=com +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: eng_admin1 +sn: Admin 1 +givenName: Engineering +mail: eng_admin1@redhat.com +userPassword: password + +# users - IT + +dn: cn=it_user1,ou=people,dc=redhat,dc=com +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: it_user1 +sn: Technology User 1 +givenName: Information +mail: it_user1@redhat.com +userPassword: password + +dn: cn=it_user2,ou=people,dc=redhat,dc=com +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: it_user2 +sn: Technology User 2 +givenName: Information +mail: it_user2@redhat.com +userPassword: password + +# users - Sales + +dn: cn=sales_user1,ou=people,dc=redhat,dc=com +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: sales_user1 +sn: Person 1 +givenName: Sales +mail: sales_user1@redhat.com +userPassword: password + +dn: cn=sales_user2,ou=people,dc=redhat,dc=com +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +cn: sales_user2 +sn: Person 2 +givenName: Sales +mail: sales_user2@redhat.com +userPassword: password diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 00728bddf6..d245ff3c7d 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -73,7 +73,8 @@ def user(): try: user = User.objects.get(username=name) except User.DoesNotExist: - user = User(username=name, is_superuser=is_superuser, password=name) + user = User(username=name, is_superuser=is_superuser) + user.set_password(name) user.save() return user return u diff --git a/awx/main/tests/functional/test_ldap.py b/awx/main/tests/functional/test_ldap.py new file mode 100644 index 0000000000..0714cc51fd --- /dev/null +++ b/awx/main/tests/functional/test_ldap.py @@ -0,0 +1,130 @@ + +import ldap +import ldif +import pytest +import os +from mockldap import MockLdap + +from awx.api.versioning import reverse + + +@pytest.fixture +def ldap_generator(): + def fn(fname, host='localhost'): + + fh = open(os.path.join(os.path.dirname(os.path.realpath(__file__)), fname), 'rb') + ctrl = ldif.LDIFRecordList(fh) + ctrl.parse() + + directory = dict(ctrl.all_records) + + mockldap = MockLdap(directory) + + mockldap.start() + mockldap['ldap://{}/'.format(host)] + + conn = ldap.initialize('ldap://{}/'.format(host)) + + return conn + #mockldap.stop() + + return fn + + +@pytest.fixture +def ldap_settings_generator(): + def fn(prefix='', dc='ansible', host='ldap.ansible.com'): + prefix = '_{}'.format(prefix) if prefix else '' + + data = { + 'AUTH_LDAP_SERVER_URI': 'ldap://{}'.format(host), + 'AUTH_LDAP_BIND_DN': 'cn=eng_user1,ou=people,dc={},dc=com'.format(dc), + 'AUTH_LDAP_BIND_PASSWORD': 'password', + "AUTH_LDAP_USER_SEARCH": [ + "ou=people,dc={},dc=com".format(dc), + "SCOPE_SUBTREE", + "(cn=%(user)s)" + ], + "AUTH_LDAP_TEAM_MAP": { + "LDAP Sales": { + "organization": "LDAP Organization", + "users": "cn=sales,ou=groups,dc={},dc=com".format(dc), + "remove": True + }, + "LDAP IT": { + "organization": "LDAP Organization", + "users": "cn=it,ou=groups,dc={},dc=com".format(dc), + "remove": True + }, + "LDAP Engineering": { + "organization": "LDAP Organization", + "users": "cn=engineering,ou=groups,dc={},dc=com".format(dc), + "remove": True + } + }, + "AUTH_LDAP_REQUIRE_GROUP": None, + "AUTH_LDAP_USER_ATTR_MAP": { + "first_name": "givenName", + "last_name": "sn", + "email": "mail" + }, + "AUTH_LDAP_GROUP_SEARCH": [ + "dc={},dc=com".format(dc), + "SCOPE_SUBTREE", + "(objectClass=groupOfNames)" + ], + "AUTH_LDAP_USER_FLAGS_BY_GROUP": { + "is_superuser": "cn=superusers,ou=groups,dc={},dc=com".format(dc) + }, + "AUTH_LDAP_ORGANIZATION_MAP": { + "LDAP Organization": { + "admins": "cn=engineering_admins,ou=groups,dc={},dc=com".format(dc), + "remove_admins": False, + "users": [ + "cn=engineering,ou=groups,dc={},dc=com".format(dc), + "cn=sales,ou=groups,dc={},dc=com".format(dc), + "cn=it,ou=groups,dc={},dc=com".format(dc) + ], + "remove_users": False + } + }, + } + + if prefix: + data_new = dict() + for k,v in data.iteritems(): + k_new = k.replace('AUTH_LDAP', 'AUTH_LDAP{}'.format(prefix)) + data_new[k_new] = v + else: + data_new = data + + return data_new + return fn + + +# Note: mockldap isn't fully featured. Fancy queries aren't fully baked. +# However, objects returned are solid so they should flow through django ldap middleware nicely. +@pytest.mark.django_db +def test_login(ldap_generator, patch, post, admin, ldap_settings_generator): + auth_url = reverse('api:auth_token_view') + ldap_settings_url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ldap'}) + + # Generate mock ldap servers and init with ldap data + ldap_generator("../data/ldap_example.ldif", "ldap.example.com") + ldap_generator("../data/ldap_redhat.ldif", "ldap.redhat.com") + ldap_generator("../data/ldap_ansible.ldif", "ldap.ansible.com") + + ldap_settings_example = ldap_settings_generator(dc='example') + ldap_settings_ansible = ldap_settings_generator(prefix='1', dc='ansible') + ldap_settings_redhat = ldap_settings_generator(prefix='2', dc='redhat') + + # eng_user1 exists in ansible and redhat but not example + patch(ldap_settings_url, user=admin, data=ldap_settings_example, expect=200) + + post(auth_url, data={'username': 'eng_user1', 'password': 'password'}, expect=400) + + patch(ldap_settings_url, user=admin, data=ldap_settings_ansible, expect=200) + patch(ldap_settings_url, user=admin, data=ldap_settings_redhat, expect=200) + + post(auth_url, data={'username': 'eng_user1', 'password': 'password'}, expect=200) + diff --git a/awx/main/tests/functional/utils/test_common.py b/awx/main/tests/functional/utils/test_common.py index 5ef89b0d06..d4121efd8f 100644 --- a/awx/main/tests/functional/utils/test_common.py +++ b/awx/main/tests/functional/utils/test_common.py @@ -20,7 +20,7 @@ def test_model_to_dict_user(alice): output_dict = model_to_dict(alice) assert output_dict['username'] == username assert output_dict['password'] == 'hidden' - assert alice.username == password + assert alice.username == username assert alice.password == password diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index a63ac39a7d..18d2ae4e88 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -305,6 +305,11 @@ REST_FRAMEWORK = { AUTHENTICATION_BACKENDS = ( 'awx.sso.backends.LDAPBackend', + 'awx.sso.backends.LDAPBackend1', + 'awx.sso.backends.LDAPBackend2', + 'awx.sso.backends.LDAPBackend3', + 'awx.sso.backends.LDAPBackend4', + 'awx.sso.backends.LDAPBackend5', 'awx.sso.backends.RADIUSBackend', 'awx.sso.backends.TACACSPlusBackend', 'social_core.backends.google.GoogleOAuth2', diff --git a/awx/sso/backends.py b/awx/sso/backends.py index 82a0dfe017..67e263d553 100644 --- a/awx/sso/backends.py +++ b/awx/sso/backends.py @@ -133,6 +133,26 @@ class LDAPBackend(BaseLDAPBackend): return set() +class LDAPBackend1(LDAPBackend): + settings_prefix = 'AUTH_LDAP_1_' + + +class LDAPBackend2(LDAPBackend): + settings_prefix = 'AUTH_LDAP_2_' + + +class LDAPBackend3(LDAPBackend): + settings_prefix = 'AUTH_LDAP_3_' + + +class LDAPBackend4(LDAPBackend): + settings_prefix = 'AUTH_LDAP_4_' + + +class LDAPBackend5(LDAPBackend): + settings_prefix = 'AUTH_LDAP_5_' + + def _decorate_enterprise_user(user, provider): user.set_unusable_password() user.save() diff --git a/awx/sso/conf.py b/awx/sso/conf.py index acaaefc7a7..54fe630585 100644 --- a/awx/sso/conf.py +++ b/awx/sso/conf.py @@ -129,271 +129,283 @@ register( # LDAP AUTHENTICATION SETTINGS ############################################################################### -register( - 'AUTH_LDAP_SERVER_URI', - field_class=fields.LDAPServerURIField, - allow_blank=True, - default='', - label=_('LDAP Server URI'), - help_text=_('URI to connect to LDAP server, such as "ldap://ldap.example.com:389" ' - '(non-SSL) or "ldaps://ldap.example.com:636" (SSL). Multiple LDAP ' - 'servers may be specified by separating with spaces or commas. LDAP ' - 'authentication is disabled if this parameter is empty.'), - category=_('LDAP'), - category_slug='ldap', - placeholder='ldaps://ldap.example.com:636', - feature_required='ldap', -) -register( - 'AUTH_LDAP_BIND_DN', - field_class=fields.CharField, - allow_blank=True, - default='', - validators=[validate_ldap_bind_dn], - label=_('LDAP Bind DN'), - help_text=_('DN (Distinguished Name) of user to bind for all search queries. This' - ' is the system user account we will use to login to query LDAP for other' - ' user information. Refer to the Ansible Tower documentation for example syntax.'), - category=_('LDAP'), - category_slug='ldap', - feature_required='ldap', -) +def _register_ldap(append=None): + append_str = '_{}'.format(append) if append else '' -register( - 'AUTH_LDAP_BIND_PASSWORD', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('LDAP Bind Password'), - help_text=_('Password used to bind LDAP user account.'), - category=_('LDAP'), - category_slug='ldap', - feature_required='ldap', - encrypted=True, -) + register( + 'AUTH_LDAP{}_SERVER_URI'.format(append_str), + field_class=fields.LDAPServerURIField, + allow_blank=True, + default='', + label=_('LDAP Server URI'), + help_text=_('URI to connect to LDAP server, such as "ldap://ldap.example.com:389" ' + '(non-SSL) or "ldaps://ldap.example.com:636" (SSL). Multiple LDAP ' + 'servers may be specified by separating with spaces or commas. LDAP ' + 'authentication is disabled if this parameter is empty.'), + category=_('LDAP'), + category_slug='ldap', + placeholder='ldaps://ldap.example.com:636', + feature_required='ldap', + ) -register( - 'AUTH_LDAP_START_TLS', - field_class=fields.BooleanField, - default=False, - label=_('LDAP Start TLS'), - help_text=_('Whether to enable TLS when the LDAP connection is not using SSL.'), - category=_('LDAP'), - category_slug='ldap', - feature_required='ldap', -) + register( + 'AUTH_LDAP{}_BIND_DN'.format(append_str), + field_class=fields.CharField, + allow_blank=True, + default='', + validators=[validate_ldap_bind_dn], + label=_('LDAP Bind DN'), + help_text=_('DN (Distinguished Name) of user to bind for all search queries. This' + ' is the system user account we will use to login to query LDAP for other' + ' user information. Refer to the Ansible Tower documentation for example syntax.'), + category=_('LDAP'), + category_slug='ldap', + feature_required='ldap', + ) -register( - 'AUTH_LDAP_CONNECTION_OPTIONS', - field_class=fields.LDAPConnectionOptionsField, - default={'OPT_REFERRALS': 0, 'OPT_NETWORK_TIMEOUT': 30}, - label=_('LDAP Connection Options'), - help_text=_('Additional options to set for the LDAP connection. LDAP ' - 'referrals are disabled by default (to prevent certain LDAP ' - 'queries from hanging with AD). Option names should be strings ' - '(e.g. "OPT_REFERRALS"). Refer to ' - 'https://www.python-ldap.org/doc/html/ldap.html#options for ' - 'possible options and values that can be set.'), - category=_('LDAP'), - category_slug='ldap', - placeholder=collections.OrderedDict([ - ('OPT_REFERRALS', 0), - ('OPT_NETWORK_TIMEOUT', 30) - ]), - feature_required='ldap', -) + register( + 'AUTH_LDAP{}_BIND_PASSWORD'.format(append_str), + field_class=fields.CharField, + allow_blank=True, + default='', + label=_('LDAP Bind Password'), + help_text=_('Password used to bind LDAP user account.'), + category=_('LDAP'), + category_slug='ldap', + feature_required='ldap', + encrypted=True, + ) -register( - 'AUTH_LDAP_USER_SEARCH', - field_class=fields.LDAPSearchUnionField, - default=[], - label=_('LDAP User Search'), - help_text=_('LDAP search query to find users. Any user that matches the given ' - 'pattern will be able to login to Tower. The user should also be ' - 'mapped into a Tower organization (as defined in the ' - 'AUTH_LDAP_ORGANIZATION_MAP setting). If multiple search queries ' - 'need to be supported use of "LDAPUnion" is possible. See ' - 'Tower documentation for details.'), - category=_('LDAP'), - category_slug='ldap', - placeholder=( - 'OU=Users,DC=example,DC=com', - 'SCOPE_SUBTREE', - '(sAMAccountName=%(user)s)', - ), - feature_required='ldap', -) + register( + 'AUTH_LDAP{}_START_TLS'.format(append_str), + field_class=fields.BooleanField, + default=False, + label=_('LDAP Start TLS'), + help_text=_('Whether to enable TLS when the LDAP connection is not using SSL.'), + category=_('LDAP'), + category_slug='ldap', + feature_required='ldap', + ) -register( - 'AUTH_LDAP_USER_DN_TEMPLATE', - field_class=fields.LDAPDNWithUserField, - allow_blank=True, - allow_null=True, - default=None, - label=_('LDAP User DN Template'), - help_text=_('Alternative to user search, if user DNs are all of the same ' - 'format. This approach is more efficient for user lookups than ' - 'searching if it is usable in your organizational environment. If ' - 'this setting has a value it will be used instead of ' - 'AUTH_LDAP_USER_SEARCH.'), - category=_('LDAP'), - category_slug='ldap', - placeholder='uid=%(user)s,OU=Users,DC=example,DC=com', - feature_required='ldap', -) + register( + 'AUTH_LDAP{}_CONNECTION_OPTIONS'.format(append_str), + field_class=fields.LDAPConnectionOptionsField, + default={'OPT_REFERRALS': 0, 'OPT_NETWORK_TIMEOUT': 30}, + label=_('LDAP Connection Options'), + help_text=_('Additional options to set for the LDAP connection. LDAP ' + 'referrals are disabled by default (to prevent certain LDAP ' + 'queries from hanging with AD). Option names should be strings ' + '(e.g. "OPT_REFERRALS"). Refer to ' + 'https://www.python-ldap.org/doc/html/ldap.html#options for ' + 'possible options and values that can be set.'), + category=_('LDAP'), + category_slug='ldap', + placeholder=collections.OrderedDict([ + ('OPT_REFERRALS', 0), + ('OPT_NETWORK_TIMEOUT', 30) + ]), + feature_required='ldap', + ) -register( - 'AUTH_LDAP_USER_ATTR_MAP', - field_class=fields.LDAPUserAttrMapField, - default={}, - label=_('LDAP User Attribute Map'), - help_text=_('Mapping of LDAP user schema to Tower API user attributes. The default' - ' setting is valid for ActiveDirectory but users with other LDAP' - ' configurations may need to change the values. Refer to the Ansible' - ' Tower documentation for additonal details.'), - category=_('LDAP'), - category_slug='ldap', - placeholder=collections.OrderedDict([ - ('first_name', 'givenName'), - ('last_name', 'sn'), - ('email', 'mail'), - ]), - feature_required='ldap', -) + register( + 'AUTH_LDAP{}_USER_SEARCH'.format(append_str), + field_class=fields.LDAPSearchUnionField, + default=[], + label=_('LDAP User Search'), + help_text=_('LDAP search query to find users. Any user that matches the given ' + 'pattern will be able to login to Tower. The user should also be ' + 'mapped into a Tower organization (as defined in the ' + 'AUTH_LDAP_ORGANIZATION_MAP setting). If multiple search queries ' + 'need to be supported use of "LDAPUnion" is possible. See ' + 'Tower documentation for details.'), + category=_('LDAP'), + category_slug='ldap', + placeholder=( + 'OU=Users,DC=example,DC=com', + 'SCOPE_SUBTREE', + '(sAMAccountName=%(user)s)', + ), + feature_required='ldap', + ) -register( - 'AUTH_LDAP_GROUP_SEARCH', - field_class=fields.LDAPSearchField, - default=[], - label=_('LDAP Group Search'), - help_text=_('Users are mapped to organizations based on their membership in LDAP' - ' groups. This setting defines the LDAP search query to find groups. ' - 'Unlike the user search, group search does not support LDAPSearchUnion.'), - category=_('LDAP'), - category_slug='ldap', - placeholder=( - 'DC=example,DC=com', - 'SCOPE_SUBTREE', - '(objectClass=group)', - ), - feature_required='ldap', -) + register( + 'AUTH_LDAP{}_USER_DN_TEMPLATE'.format(append_str), + field_class=fields.LDAPDNWithUserField, + allow_blank=True, + allow_null=True, + default=None, + label=_('LDAP User DN Template'), + help_text=_('Alternative to user search, if user DNs are all of the same ' + 'format. This approach is more efficient for user lookups than ' + 'searching if it is usable in your organizational environment. If ' + 'this setting has a value it will be used instead of ' + 'AUTH_LDAP_USER_SEARCH.'), + category=_('LDAP'), + category_slug='ldap', + placeholder='uid=%(user)s,OU=Users,DC=example,DC=com', + feature_required='ldap', + ) -register( - 'AUTH_LDAP_GROUP_TYPE', - field_class=fields.LDAPGroupTypeField, - label=_('LDAP Group Type'), - help_text=_('The group type may need to be changed based on the type of the ' - 'LDAP server. Values are listed at: ' - 'https://django-auth-ldap.readthedocs.io/en/stable/groups.html#types-of-groups'), - category=_('LDAP'), - category_slug='ldap', - feature_required='ldap', - default='MemberDNGroupType', -) + register( + 'AUTH_LDAP{}_USER_ATTR_MAP'.format(append_str), + field_class=fields.LDAPUserAttrMapField, + default={}, + label=_('LDAP User Attribute Map'), + help_text=_('Mapping of LDAP user schema to Tower API user attributes. The default' + ' setting is valid for ActiveDirectory but users with other LDAP' + ' configurations may need to change the values. Refer to the Ansible' + ' Tower documentation for additonal details.'), + category=_('LDAP'), + category_slug='ldap', + placeholder=collections.OrderedDict([ + ('first_name', 'givenName'), + ('last_name', 'sn'), + ('email', 'mail'), + ]), + feature_required='ldap', + ) -register( - 'AUTH_LDAP_REQUIRE_GROUP', - field_class=fields.LDAPDNField, - allow_blank=True, - allow_null=True, - default=None, - label=_('LDAP Require Group'), - help_text=_('Group DN required to login. If specified, user must be a member ' - 'of this group to login via LDAP. If not set, everyone in LDAP ' - 'that matches the user search will be able to login via Tower. ' - 'Only one require group is supported.'), - category=_('LDAP'), - category_slug='ldap', - placeholder='CN=Tower Users,OU=Users,DC=example,DC=com', - feature_required='ldap', -) + register( + 'AUTH_LDAP{}_GROUP_SEARCH'.format(append_str), + field_class=fields.LDAPSearchField, + default=[], + label=_('LDAP Group Search'), + help_text=_('Users are mapped to organizations based on their membership in LDAP' + ' groups. This setting defines the LDAP search query to find groups. ' + 'Unlike the user search, group search does not support LDAPSearchUnion.'), + category=_('LDAP'), + category_slug='ldap', + placeholder=( + 'DC=example,DC=com', + 'SCOPE_SUBTREE', + '(objectClass=group)', + ), + feature_required='ldap', + ) -register( - 'AUTH_LDAP_DENY_GROUP', - field_class=fields.LDAPDNField, - allow_blank=True, - allow_null=True, - default=None, - label=_('LDAP Deny Group'), - help_text=_('Group DN denied from login. If specified, user will not be ' - 'allowed to login if a member of this group. Only one deny group ' - 'is supported.'), - category=_('LDAP'), - category_slug='ldap', - placeholder='CN=Disabled Users,OU=Users,DC=example,DC=com', - feature_required='ldap', -) + register( + 'AUTH_LDAP{}_GROUP_TYPE'.format(append_str), + field_class=fields.LDAPGroupTypeField, + label=_('LDAP Group Type'), + help_text=_('The group type may need to be changed based on the type of the ' + 'LDAP server. Values are listed at: ' + 'https://django-auth-ldap.readthedocs.io/en/stable/groups.html#types-of-groups'), + category=_('LDAP'), + category_slug='ldap', + feature_required='ldap', + default='MemberDNGroupType', + ) -register( - 'AUTH_LDAP_USER_FLAGS_BY_GROUP', - field_class=fields.LDAPUserFlagsField, - default={}, - label=_('LDAP User Flags By Group'), - help_text=_('Retrieve users from a given group. At this time, superuser and system' - ' auditors are the only groups supported. Refer to the Ansible Tower' - ' documentation for more detail.'), - category=_('LDAP'), - category_slug='ldap', - placeholder=collections.OrderedDict([ - ('is_superuser', 'CN=Domain Admins,CN=Users,DC=example,DC=com'), - ('is_system_auditor', 'CN=Domain Auditors,CN=Users,DC=example,DC=com'), - ]), - feature_required='ldap', -) + register( + 'AUTH_LDAP{}_REQUIRE_GROUP'.format(append_str), + field_class=fields.LDAPDNField, + allow_blank=True, + allow_null=True, + default=None, + label=_('LDAP Require Group'), + help_text=_('Group DN required to login. If specified, user must be a member ' + 'of this group to login via LDAP. If not set, everyone in LDAP ' + 'that matches the user search will be able to login via Tower. ' + 'Only one require group is supported.'), + category=_('LDAP'), + category_slug='ldap', + placeholder='CN=Tower Users,OU=Users,DC=example,DC=com', + feature_required='ldap', + ) -register( - 'AUTH_LDAP_ORGANIZATION_MAP', - field_class=fields.LDAPOrganizationMapField, - default={}, - label=_('LDAP Organization Map'), - help_text=_('Mapping between organization admins/users and LDAP groups. This ' - 'controls which users are placed into which Tower organizations ' - 'relative to their LDAP group memberships. Configuration details ' - 'are available in the Ansible Tower documentation.'), - category=_('LDAP'), - category_slug='ldap', - placeholder=collections.OrderedDict([ - ('Test Org', collections.OrderedDict([ - ('admins', 'CN=Domain Admins,CN=Users,DC=example,DC=com'), - ('users', ['CN=Domain Users,CN=Users,DC=example,DC=com']), - ('remove_users', True), - ('remove_admins', True), - ])), - ('Test Org 2', collections.OrderedDict([ - ('admins', 'CN=Administrators,CN=Builtin,DC=example,DC=com'), - ('users', True), - ('remove_users', True), - ('remove_admins', True), - ])), - ]), - feature_required='ldap', -) + register( + 'AUTH_LDAP{}_DENY_GROUP'.format(append_str), + field_class=fields.LDAPDNField, + allow_blank=True, + allow_null=True, + default=None, + label=_('LDAP Deny Group'), + help_text=_('Group DN denied from login. If specified, user will not be ' + 'allowed to login if a member of this group. Only one deny group ' + 'is supported.'), + category=_('LDAP'), + category_slug='ldap', + placeholder='CN=Disabled Users,OU=Users,DC=example,DC=com', + feature_required='ldap', + ) -register( - 'AUTH_LDAP_TEAM_MAP', - field_class=fields.LDAPTeamMapField, - default={}, - label=_('LDAP Team Map'), - help_text=_('Mapping between team members (users) and LDAP groups. Configuration' - ' details are available in the Ansible Tower documentation.'), - category=_('LDAP'), - category_slug='ldap', - placeholder=collections.OrderedDict([ - ('My Team', collections.OrderedDict([ - ('organization', 'Test Org'), - ('users', ['CN=Domain Users,CN=Users,DC=example,DC=com']), - ('remove', True), - ])), - ('Other Team', collections.OrderedDict([ - ('organization', 'Test Org 2'), - ('users', 'CN=Other Users,CN=Users,DC=example,DC=com'), - ('remove', False), - ])), - ]), - feature_required='ldap', -) + register( + 'AUTH_LDAP{}_USER_FLAGS_BY_GROUP'.format(append_str), + field_class=fields.LDAPUserFlagsField, + default={}, + label=_('LDAP User Flags By Group'), + help_text=_('Retrieve users from a given group. At this time, superuser and system' + ' auditors are the only groups supported. Refer to the Ansible Tower' + ' documentation for more detail.'), + category=_('LDAP'), + category_slug='ldap', + placeholder=collections.OrderedDict([ + ('is_superuser', 'CN=Domain Admins,CN=Users,DC=example,DC=com'), + ('is_system_auditor', 'CN=Domain Auditors,CN=Users,DC=example,DC=com'), + ]), + feature_required='ldap', + ) + + register( + 'AUTH_LDAP{}_ORGANIZATION_MAP'.format(append_str), + field_class=fields.LDAPOrganizationMapField, + default={}, + label=_('LDAP Organization Map'), + help_text=_('Mapping between organization admins/users and LDAP groups. This ' + 'controls which users are placed into which Tower organizations ' + 'relative to their LDAP group memberships. Configuration details ' + 'are available in the Ansible Tower documentation.'), + category=_('LDAP'), + category_slug='ldap', + placeholder=collections.OrderedDict([ + ('Test Org', collections.OrderedDict([ + ('admins', 'CN=Domain Admins,CN=Users,DC=example,DC=com'), + ('users', ['CN=Domain Users,CN=Users,DC=example,DC=com']), + ('remove_users', True), + ('remove_admins', True), + ])), + ('Test Org 2', collections.OrderedDict([ + ('admins', 'CN=Administrators,CN=Builtin,DC=example,DC=com'), + ('users', True), + ('remove_users', True), + ('remove_admins', True), + ])), + ]), + feature_required='ldap', + ) + + register( + 'AUTH_LDAP{}_TEAM_MAP'.format(append_str), + field_class=fields.LDAPTeamMapField, + default={}, + label=_('LDAP Team Map'), + help_text=_('Mapping between team members (users) and LDAP groups. Configuration' + ' details are available in the Ansible Tower documentation.'), + category=_('LDAP'), + category_slug='ldap', + placeholder=collections.OrderedDict([ + ('My Team', collections.OrderedDict([ + ('organization', 'Test Org'), + ('users', ['CN=Domain Users,CN=Users,DC=example,DC=com']), + ('remove', True), + ])), + ('Other Team', collections.OrderedDict([ + ('organization', 'Test Org 2'), + ('users', 'CN=Other Users,CN=Users,DC=example,DC=com'), + ('remove', False), + ])), + ]), + feature_required='ldap', + ) + + +_register_ldap() +_register_ldap('1') +_register_ldap('2') +_register_ldap('3') +_register_ldap('4') +_register_ldap('5') ############################################################################### # RADIUS AUTHENTICATION SETTINGS diff --git a/awx/sso/fields.py b/awx/sso/fields.py index a483044464..8df0bb7a53 100644 --- a/awx/sso/fields.py +++ b/awx/sso/fields.py @@ -31,6 +31,21 @@ class AuthenticationBackendsField(fields.StringListField): ('awx.sso.backends.LDAPBackend', [ 'AUTH_LDAP_SERVER_URI', ]), + ('awx.sso.backends.LDAPBackend1', [ + 'AUTH_LDAP_1_SERVER_URI', + ]), + ('awx.sso.backends.LDAPBackend2', [ + 'AUTH_LDAP_2_SERVER_URI', + ]), + ('awx.sso.backends.LDAPBackend3', [ + 'AUTH_LDAP_3_SERVER_URI', + ]), + ('awx.sso.backends.LDAPBackend4', [ + 'AUTH_LDAP_4_SERVER_URI', + ]), + ('awx.sso.backends.LDAPBackend5', [ + 'AUTH_LDAP_5_SERVER_URI', + ]), ('awx.sso.backends.RADIUSBackend', [ 'RADIUS_SERVER', ]), @@ -70,6 +85,11 @@ class AuthenticationBackendsField(fields.StringListField): REQUIRED_BACKEND_FEATURE = { 'awx.sso.backends.LDAPBackend': 'ldap', + 'awx.sso.backends.LDAPBackend1': 'ldap', + 'awx.sso.backends.LDAPBackend2': 'ldap', + 'awx.sso.backends.LDAPBackend3': 'ldap', + 'awx.sso.backends.LDAPBackend4': 'ldap', + 'awx.sso.backends.LDAPBackend5': 'ldap', 'awx.sso.backends.RADIUSBackend': 'enterprise_auth', 'awx.sso.backends.SAMLAuth': 'enterprise_auth', } diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt index eb0b6a8bd5..aa3edffe6b 100644 --- a/requirements/requirements_dev.txt +++ b/requirements/requirements_dev.txt @@ -16,3 +16,4 @@ uwsgitop jupyter matplotlib backports.tempfile # support in unit tests for py32+ tempfile.TemporaryDirectory +mockldap