diff --git a/ci/Dockerfile b/ci/Dockerfile index 50b3195..dfbd0d6 100644 --- a/ci/Dockerfile +++ b/ci/Dockerfile @@ -14,6 +14,8 @@ RUN yum install -y \ python3-devel \ python3-pip \ python3-setuptools \ + gcc \ + openldap-devel \ && pip3 install --no-cache-dir --upgrade setuptools pip \ && pip3 install --no-cache-dir \ kubernetes \ diff --git a/meta/runtime.yml b/meta/runtime.yml index fd6bf1b..8d843b1 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -7,3 +7,5 @@ plugin_routing: action: k8s: redirect: kubernetes.core.k8s_info + openshift_adm_groups_sync: + redirect: kubernetes.core.k8s_info diff --git a/molecule/default/prepare.yml b/molecule/default/prepare.yml index 9d26da9..28cfce9 100644 --- a/molecule/default/prepare.yml +++ b/molecule/default/prepare.yml @@ -12,6 +12,7 @@ name: - kubernetes>=12.0.0 - coverage + - python-ldap virtualenv: "{{ virtualenv }}" virtualenv_command: "{{ virtualenv_command }}" virtualenv_site_packages: no diff --git a/molecule/default/roles/openshift_adm_groups/defaults/main.yml b/molecule/default/roles/openshift_adm_groups/defaults/main.yml new file mode 100644 index 0000000..b58e2d3 --- /dev/null +++ b/molecule/default/roles/openshift_adm_groups/defaults/main.yml @@ -0,0 +1,4 @@ +--- +ldap_admin_user: "admin" +ldap_admin_password: "testing123!" +ldap_root: "dc=ansible,dc=redhat" \ No newline at end of file diff --git a/molecule/default/roles/openshift_adm_groups/library/openshift_ldap_entry.py b/molecule/default/roles/openshift_adm_groups/library/openshift_ldap_entry.py new file mode 100644 index 0000000..6f5ca4b --- /dev/null +++ b/molecule/default/roles/openshift_adm_groups/library/openshift_ldap_entry.py @@ -0,0 +1,186 @@ +# 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 + +DOCUMENTATION = r''' + +module: openshift_ldap_entry + +short_description: add/remove entry to LDAP Server. + +author: + - Aubin Bikouo (@abikouo) + +description: + - This module perform basic operations on the LDAP Server (add/remove entries). + - Similar to `community.general.ldap_entry` this has been created to avoid dependency with this collection for the test. + - This module is not supported outside of testing this collection. + +options: + attributes: + description: + - If I(state=present), attributes necessary to create an entry. Existing + entries are never modified. To assert specific attribute values on an + existing entry, use M(community.general.ldap_attrs) module instead. + type: dict + objectClass: + description: + - If I(state=present), value or list of values to use when creating + the entry. It can either be a string or an actual list of + strings. + type: list + elements: str + state: + description: + - The target state of the entry. + choices: [present, absent] + default: present + type: str + bind_dn: + description: + - A DN to bind with. If this is omitted, we'll try a SASL bind with the EXTERNAL mechanism as default. + - If this is blank, we'll use an anonymous bind. + type: str + required: true + bind_pw: + description: + - The password to use with I(bind_dn). + type: str + dn: + required: true + description: + - The DN of the entry to add or remove. + type: str + server_uri: + description: + - A URI to the LDAP server. + - The default value lets the underlying LDAP client library look for a UNIX domain socket in its default location. + type: str + default: ldapi:/// + +requirements: + - python-ldap +''' + +EXAMPLES = r''' +''' + + +RETURN = r''' +# Default return values +''' + +import traceback + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.common.text.converters import to_native, to_bytes + +LDAP_IMP_ERR = None +try: + import ldap + import ldap.modlist + + HAS_LDAP = True +except ImportError: + LDAP_IMP_ERR = traceback.format_exc() + HAS_LDAP = False + + +def argument_spec(): + args = {} + args['attributes'] = dict(default={}, type='dict') + args['objectClass'] = dict(type='list', elements='str') + args['state'] = dict(default='present', choices=['present', 'absent']) + args['bind_dn'] = dict(required=True) + args['bind_pw'] = dict(default='', no_log=True) + args['dn'] = dict(required=True) + args['server_uri'] = dict(default='ldapi:///') + return args + + +class LdapEntry(AnsibleModule): + def __init__(self): + + AnsibleModule.__init__( + self, + argument_spec=argument_spec(), + required_if=[('state', 'present', ['objectClass'])], + ) + + if not HAS_LDAP: + self.fail_json(msg=missing_required_lib('python-ldap'), exception=LDAP_IMP_ERR) + + self.__connection = None + # Add the objectClass into the list of attributes + self.params['attributes']['objectClass'] = (self.params['objectClass']) + + # Load attributes + if self.params['state'] == 'present': + self.attrs = {} + for name, value in self.params['attributes'].items(): + if isinstance(value, list): + self.attrs[name] = list(map(to_bytes, value)) + else: + self.attrs[name] = [to_bytes(value)] + + @property + def connection(self): + if not self.__connection: + ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) + self.__connection = ldap.initialize(self.params['server_uri']) + try: + self.__connection.simple_bind_s(self.params['bind_dn'], self.params['bind_pw']) + except ldap.LDAPError as e: + self.fail_json(msg="Cannot bind to the server due to: %s" % e) + return self.__connection + + def add(self): + """ If self.dn does not exist, returns a callable that will add it. """ + changed = False + msg = "LDAP Entry '%s' already exist." % self.params["dn"] + if not self._is_entry_present(): + modlist = ldap.modlist.addModlist(self.attrs) + self.connection.add_s(self.params['dn'], modlist) + changed = True + msg = "LDAP Entry '%s' successfully created." % self.params["dn"] + self.exit_json(changed=changed, msg=msg) + + def delete(self): + """ If self.dn exists, returns a callable that will delete it. """ + changed = False + msg = "LDAP Entry '%s' does not exist." % self.params["dn"] + if self._is_entry_present(): + self.connection.delete_s(self.params['dn']) + changed = True + msg = "LDAP Entry '%s' successfully deleted." % self.params["dn"] + self.exit_json(changed=changed, msg=msg) + + def _is_entry_present(self): + try: + self.connection.search_s(self.params['dn'], ldap.SCOPE_BASE) + except ldap.NO_SUCH_OBJECT: + is_present = False + else: + is_present = True + + return is_present + + def execute(self): + try: + if self.params['state'] == 'present': + self.add() + else: + self.delete() + except Exception as e: + self.fail_json(msg="Entry action failed.", details=to_native(e), exception=traceback.format_exc()) + + +def main(): + module = LdapEntry() + module.execute() + + +if __name__ == '__main__': + main() diff --git a/molecule/default/roles/openshift_adm_groups/library/openshift_ldap_entry_info.py b/molecule/default/roles/openshift_adm_groups/library/openshift_ldap_entry_info.py new file mode 100644 index 0000000..ba49f72 --- /dev/null +++ b/molecule/default/roles/openshift_adm_groups/library/openshift_ldap_entry_info.py @@ -0,0 +1,109 @@ +# 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 + +DOCUMENTATION = r''' + +module: openshift_ldap_entry_info + +short_description: Validate entry from LDAP server. + +author: + - Aubin Bikouo (@abikouo) + +description: + - This module connect to a ldap server and search for entry. + - This module is not supported outside of testing this collection. + +options: + bind_dn: + description: + - A DN to bind with. If this is omitted, we'll try a SASL bind with the EXTERNAL mechanism as default. + - If this is blank, we'll use an anonymous bind. + type: str + required: true + bind_pw: + description: + - The password to use with I(bind_dn). + type: str + required: True + dn: + description: + - The DN of the entry to test. + type: str + required: True + server_uri: + description: + - A URI to the LDAP server. + - The default value lets the underlying LDAP client library look for a UNIX domain socket in its default location. + type: str + default: ldapi:/// + required: True + +requirements: + - python-ldap +''' + +EXAMPLES = r''' +''' + + +RETURN = r''' +# Default return values +''' + +import traceback + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib + +LDAP_IMP_ERR = None +try: + import ldap + import ldap.modlist + HAS_LDAP = True +except ImportError: + LDAP_IMP_ERR = traceback.format_exc() + HAS_LDAP = False + + +def argument_spec(): + args = {} + args['bind_dn'] = dict(required=True) + args['bind_pw'] = dict(required=True, no_log=True) + args['dn'] = dict(required=True) + args['server_uri'] = dict(required=True) + return args + + +def execute(): + module = AnsibleModule( + argument_spec=argument_spec(), + supports_check_mode=True + ) + + if not HAS_LDAP: + module.fail_json(msg=missing_required_lib("python-ldap"), exception=LDAP_IMP_ERR) + + ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) + connection = ldap.initialize(module.params['server_uri']) + try: + connection.simple_bind_s(module.params['bind_dn'], module.params['bind_pw']) + except ldap.LDAPError as e: + module.fail_json(msg="Cannot bind to the server due to: %s" % e) + + try: + connection.search_s(module.params['dn'], ldap.SCOPE_BASE) + module.exit_json(changed=False, found=True) + except ldap.NO_SUCH_OBJECT: + module.exit_json(changed=False, found=False) + + +def main(): + execute() + + +if __name__ == '__main__': + main() diff --git a/molecule/default/roles/openshift_adm_groups/meta/main.yml b/molecule/default/roles/openshift_adm_groups/meta/main.yml new file mode 100644 index 0000000..eef26f2 --- /dev/null +++ b/molecule/default/roles/openshift_adm_groups/meta/main.yml @@ -0,0 +1,4 @@ +--- +collections: + - community.okd + - kubernetes.core \ No newline at end of file diff --git a/molecule/default/roles/openshift_adm_groups/tasks/activeDirectory.yml b/molecule/default/roles/openshift_adm_groups/tasks/activeDirectory.yml new file mode 100644 index 0000000..da99f32 --- /dev/null +++ b/molecule/default/roles/openshift_adm_groups/tasks/activeDirectory.yml @@ -0,0 +1,235 @@ +- block: + - name: Get LDAP definition + set_fact: + ldap_entries: "{{ lookup('template', 'ad/definition.j2') | from_yaml }}" + + - name: Delete openshift groups if existing + community.okd.k8s: + state: absent + kind: Group + version: "user.openshift.io/v1" + name: "{{ item }}" + with_items: + - admins + - developers + + - name: Delete existing LDAP Entries + openshift_ldap_entry: + bind_dn: "{{ ldap_bind_dn }}" + bind_pw: "{{ ldap_bind_pw }}" + server_uri: "{{ ldap_server_uri }}" + dn: "{{ item.dn }}" + state: absent + with_items: "{{ ldap_entries.users + ldap_entries.units | reverse | list }}" + + - name: Create LDAP Entries + openshift_ldap_entry: + bind_dn: "{{ ldap_bind_dn }}" + bind_pw: "{{ ldap_bind_pw }}" + server_uri: "{{ ldap_server_uri }}" + dn: "{{ item.dn }}" + attributes: "{{ item.attr }}" + objectClass: "{{ item.class }}" + with_items: "{{ ldap_entries.units + ldap_entries.users }}" + + - name: Load test configurations + set_fact: + sync_config: "{{ lookup('template', 'ad/sync-config.j2') | from_yaml }}" + + - name: Synchronize Groups + community.okd.openshift_adm_groups_sync: + config: "{{ sync_config }}" + check_mode: yes + register: result + + - name: Validate Group going to be created + assert: + that: + - result is changed + - admins_group + - devs_group + - '"jane.smith@ansible.org" in {{ admins_group.users }}' + - '"jim.adams@ansible.org" in {{ admins_group.users }}' + - '"jordanbulls@ansible.org" in {{ devs_group.users }}' + - admins_group.users | length == 2 + - devs_group.users | length == 1 + vars: + admins_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'admins') | first }}" + devs_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'developers') | first }}" + + + - name: Synchronize Groups (Remove check_mode) + community.okd.openshift_adm_groups_sync: + config: "{{ sync_config }}" + register: result + + - name: Validate Group going to be created + assert: + that: + - result is changed + + - name: Read admins group + kubernetes.core.k8s_info: + kind: Group + version: "user.openshift.io/v1" + name: admins + register: result + + - name: Validate group was created + assert: + that: + - result.resources | length == 1 + - '"jane.smith@ansible.org" in {{ result.resources.0.users }}' + - '"jim.adams@ansible.org" in {{ result.resources.0.users }}' + + - name: Read developers group + kubernetes.core.k8s_info: + kind: Group + version: "user.openshift.io/v1" + name: developers + register: result + + - name: Validate group was created + assert: + that: + - result.resources | length == 1 + - '"jordanbulls@ansible.org" in {{ result.resources.0.users }}' + + - name: Define user dn to delete + set_fact: + user_to_delete: "cn=Jane,ou=engineers,ou=activeD,{{ ldap_root }}" + + - name: Delete 1 admin user + openshift_ldap_entry: + bind_dn: "{{ ldap_bind_dn }}" + bind_pw: "{{ ldap_bind_pw }}" + server_uri: "{{ ldap_server_uri }}" + dn: "{{ user_to_delete }}" + state: absent + + - name: Synchronize Openshift groups using allow_groups + community.okd.openshift_adm_groups_sync: + config: "{{ sync_config }}" + allow_groups: + - developers + type: openshift + register: openshift_sync + + - name: Validate that only developers group was sync + assert: + that: + - openshift_sync is changed + - openshift_sync.groups | length == 1 + - openshift_sync.groups.0.metadata.name == "developers" + + - name: Read admins group + kubernetes.core.k8s_info: + kind: Group + version: "user.openshift.io/v1" + name: admins + register: result + + - name: Validate admins group content has not changed + assert: + that: + - result.resources | length == 1 + - '"jane.smith@ansible.org" in {{ result.resources.0.users }}' + - '"jim.adams@ansible.org" in {{ result.resources.0.users }}' + + - name: Synchronize Openshift groups using deny_groups + community.okd.openshift_adm_groups_sync: + config: "{{ sync_config }}" + deny_groups: + - developers + type: openshift + register: openshift_sync + + - name: Validate that only admins group was sync + assert: + that: + - openshift_sync is changed + - openshift_sync.groups | length == 1 + - openshift_sync.groups.0.metadata.name == "admins" + + - name: Read admins group + kubernetes.core.k8s_info: + kind: Group + version: "user.openshift.io/v1" + name: admins + register: result + + - name: Validate admins group contains only 1 user now + assert: + that: + - result.resources | length == 1 + - result.resources.0.users == ["jim.adams@ansible.org"] + + - name: Set users to delete (delete all developers users) + set_fact: + user_to_delete: "cn=Jordan,ou=engineers,ou=activeD,{{ ldap_root }}" + + - name: Delete 1 admin user + openshift_ldap_entry: + bind_dn: "{{ ldap_bind_dn }}" + bind_pw: "{{ ldap_bind_pw }}" + server_uri: "{{ ldap_server_uri }}" + dn: "{{ user_to_delete }}" + state: absent + + - name: Prune groups + community.okd.openshift_adm_groups_sync: + config: "{{ sync_config }}" + state: absent + register: result + + - name: Validate result is changed (only developers group be deleted) + assert: + that: + - result is changed + - result.groups | length == 1 + + - name: Get developers group info + kubernetes.core.k8s_info: + kind: Group + version: "user.openshift.io/v1" + name: developers + register: result + + - name: assert group was deleted + assert: + that: + - result.resources | length == 0 + + - name: Get admins group info + kubernetes.core.k8s_info: + kind: Group + version: "user.openshift.io/v1" + name: admins + register: result + + - name: assert group was not deleted + assert: + that: + - result.resources | length == 1 + + - name: Prune groups once again (idempotency) + community.okd.openshift_adm_groups_sync: + config: "{{ sync_config }}" + state: absent + register: result + + - name: Assert nothing was changed + assert: + that: + - result is not changed + + always: + - name: Delete openshift groups if existing + community.okd.k8s: + state: absent + kind: Group + version: "user.openshift.io/v1" + name: "{{ item }}" + with_items: + - admins + - developers diff --git a/molecule/default/roles/openshift_adm_groups/tasks/augmentedActiveDirectory.yml b/molecule/default/roles/openshift_adm_groups/tasks/augmentedActiveDirectory.yml new file mode 100644 index 0000000..f70d3bd --- /dev/null +++ b/molecule/default/roles/openshift_adm_groups/tasks/augmentedActiveDirectory.yml @@ -0,0 +1,174 @@ +- block: + - name: Get LDAP definition + set_fact: + ldap_entries: "{{ lookup('template', 'augmented-ad/definition.j2') | from_yaml }}" + + - name: Delete openshift groups if existing + community.okd.k8s: + state: absent + kind: Group + version: "user.openshift.io/v1" + name: "{{ item }}" + with_items: + - banking + - insurance + + - name: Delete existing LDAP entries + openshift_ldap_entry: + bind_dn: "{{ ldap_bind_dn }}" + bind_pw: "{{ ldap_bind_pw }}" + server_uri: "{{ ldap_server_uri }}" + dn: "{{ item.dn }}" + state: absent + with_items: "{{ ldap_entries.users + ldap_entries.groups + ldap_entries.units | reverse | list }}" + + - name: Create LDAP Entries + openshift_ldap_entry: + bind_dn: "{{ ldap_bind_dn }}" + bind_pw: "{{ ldap_bind_pw }}" + server_uri: "{{ ldap_server_uri }}" + dn: "{{ item.dn }}" + attributes: "{{ item.attr }}" + objectClass: "{{ item.class }}" + with_items: "{{ ldap_entries.units + ldap_entries.groups + ldap_entries.users }}" + + - name: Load test configurations + set_fact: + sync_config: "{{ lookup('template', 'augmented-ad/sync-config.j2') | from_yaml }}" + + - name: Synchronize Groups + community.okd.openshift_adm_groups_sync: + config: "{{ sync_config }}" + check_mode: yes + register: result + + - name: Validate that 'banking' and 'insurance' groups were created + assert: + that: + - result is changed + - banking_group + - insurance_group + - '"james-allan@ansible.org" in {{ banking_group.users }}' + - '"gordon-kane@ansible.org" in {{ banking_group.users }}' + - '"alice-courtney@ansible.org" in {{ insurance_group.users }}' + - banking_group.users | length == 2 + - insurance_group.users | length == 1 + vars: + banking_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'banking') | first }}" + insurance_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'insurance') | first }}" + + + - name: Synchronize Groups (Remove check_mode) + community.okd.openshift_adm_groups_sync: + config: "{{ sync_config }}" + register: result + + - name: Validate Group going to be created + assert: + that: + - result is changed + + - name: Define facts for group to create + set_fact: + ldap_groups: + - name: banking + users: + - "james-allan@ansible.org" + - "gordon-kane@ansible.org" + - name: insurance + users: + - "alice-courtney@ansible.org" + + + - name: Read 'banking' openshift group + kubernetes.core.k8s_info: + kind: Group + version: "user.openshift.io/v1" + name: banking + register: result + + - name: Validate group info + assert: + that: + - result.resources | length == 1 + - '"james-allan@ansible.org" in {{ result.resources.0.users }}' + - '"gordon-kane@ansible.org" in {{ result.resources.0.users }}' + + - name: Read 'insurance' openshift group + kubernetes.core.k8s_info: + kind: Group + version: "user.openshift.io/v1" + name: insurance + register: result + + - name: Validate group info + assert: + that: + - result.resources | length == 1 + - 'result.resources.0.users == ["alice-courtney@ansible.org"]' + + - name: Delete employee from 'insurance' group + openshift_ldap_entry: + bind_dn: "{{ ldap_bind_dn }}" + bind_pw: "{{ ldap_bind_pw }}" + server_uri: "{{ ldap_server_uri }}" + dn: "cn=Alice,ou=employee,ou=augmentedAD,{{ ldap_root }}" + state: absent + + - name: Prune groups + community.okd.openshift_adm_groups_sync: + config: "{{ sync_config }}" + state: absent + register: result + + - name: Validate result is changed (only insurance group be deleted) + assert: + that: + - result is changed + - result.groups | length == 1 + + - name: Get 'insurance' openshift group info + kubernetes.core.k8s_info: + kind: Group + version: "user.openshift.io/v1" + name: insurance + register: result + + - name: assert group was deleted + assert: + that: + - result.resources | length == 0 + + - name: Get 'banking' openshift group info + kubernetes.core.k8s_info: + kind: Group + version: "user.openshift.io/v1" + name: banking + register: result + + - name: assert group was not deleted + assert: + that: + - result.resources | length == 1 + + - name: Prune groups once again (idempotency) + community.okd.openshift_adm_groups_sync: + config: "{{ sync_config }}" + state: absent + register: result + + - name: Assert no change was made + assert: + that: + - result is not changed + + always: + - name: Delete openshift groups if existing + community.okd.k8s: + state: absent + kind: Group + version: "user.openshift.io/v1" + name: "{{ item }}" + with_items: + - banking + - insurance \ No newline at end of file diff --git a/molecule/default/roles/openshift_adm_groups/tasks/main.yml b/molecule/default/roles/openshift_adm_groups/tasks/main.yml new file mode 100644 index 0000000..bb823bc --- /dev/null +++ b/molecule/default/roles/openshift_adm_groups/tasks/main.yml @@ -0,0 +1,61 @@ +--- +- name: Get cluster information + kubernetes.core.k8s_cluster_info: + register: info + +- name: Create LDAP Pod + community.okd.k8s: + namespace: "default" + wait: yes + definition: + kind: Pod + apiVersion: v1 + metadata: + name: ldap-pod + labels: + app: ldap + spec: + containers: + - name: ldap + image: bitnami/openldap + env: + - name: LDAP_ADMIN_USERNAME + value: "{{ ldap_admin_user }}" + - name: LDAP_ADMIN_PASSWORD + value: "{{ ldap_admin_password }}" + - name: LDAP_USERS + value: "ansible" + - name: LDAP_PASSWORDS + value: "ansible123" + - name: LDAP_ROOT + value: "{{ ldap_root }}" + ports: + - containerPort: 1389 + register: pod_info + +- name: Set Pod Internal IP + set_fact: + podIp: "{{ pod_info.result.status.podIP }}" + +- name: Set LDAP Common facts + set_fact: + ldap_server_uri: "ldap://{{ podIp }}:1389" + ldap_bind_dn: "cn={{ ldap_admin_user }},{{ ldap_root }}" + ldap_bind_pw: "{{ ldap_admin_password }}" + +- name: Display LDAP Server URI + debug: + var: ldap_server_uri + +- name: Test existing user from LDAP server + openshift_ldap_entry_info: + bind_dn: "{{ ldap_bind_dn }}" + bind_pw: "{{ ldap_bind_pw }}" + dn: "ou=users,{{ ldap_root }}" + server_uri: "{{ ldap_server_uri }}" + # ignore_errors: true + # register: ping_ldap + +- include_tasks: "tasks/rfc2307.yml" +- include_tasks: "tasks/activeDirectory.yml" +- include_tasks: "tasks/augmentedActiveDirectory.yml" diff --git a/molecule/default/roles/openshift_adm_groups/tasks/rfc2307.yml b/molecule/default/roles/openshift_adm_groups/tasks/rfc2307.yml new file mode 100644 index 0000000..7660bf6 --- /dev/null +++ b/molecule/default/roles/openshift_adm_groups/tasks/rfc2307.yml @@ -0,0 +1,468 @@ +- block: + - name: Get LDAP definition + set_fact: + ldap_resources: "{{ lookup('template', 'rfc2307/definition.j2') | from_yaml }}" + + - name: Delete openshift groups if existing + community.okd.k8s: + state: absent + kind: Group + version: "user.openshift.io/v1" + name: "{{ item }}" + with_items: + - admins + - engineers + - developers + + - name: Delete existing LDAP entries + openshift_ldap_entry: + bind_dn: "{{ ldap_bind_dn }}" + bind_pw: "{{ ldap_bind_pw }}" + server_uri: "{{ ldap_server_uri }}" + dn: "{{ item.dn }}" + state: absent + with_items: "{{ ldap_resources.users + ldap_resources.groups + ldap_resources.units | reverse | list }}" + + - name: Create LDAP units + openshift_ldap_entry: + bind_dn: "{{ ldap_bind_dn }}" + bind_pw: "{{ ldap_bind_pw }}" + server_uri: "{{ ldap_server_uri }}" + dn: "{{ item.dn }}" + attributes: "{{ item.attr }}" + objectClass: "{{ item.class }}" + with_items: "{{ ldap_resources.units }}" + + - name: Create LDAP Groups + openshift_ldap_entry: + bind_dn: "{{ ldap_bind_dn }}" + bind_pw: "{{ ldap_bind_pw }}" + server_uri: "{{ ldap_server_uri }}" + dn: "{{ item.dn }}" + attributes: "{{ item.attr }}" + objectClass: "{{ item.class }}" + with_items: "{{ ldap_resources.groups }}" + + - name: Create LDAP users + openshift_ldap_entry: + bind_dn: "{{ ldap_bind_dn }}" + bind_pw: "{{ ldap_bind_pw }}" + server_uri: "{{ ldap_server_uri }}" + dn: "{{ item.dn }}" + attributes: "{{ item.attr }}" + objectClass: "{{ item.class }}" + with_items: "{{ ldap_resources.users }}" + + - name: Load test configurations + set_fact: + configs: "{{ lookup('template', 'rfc2307/sync-config.j2') | from_yaml }}" + + - name: Synchronize Groups + community.okd.openshift_adm_groups_sync: + config: "{{ configs.simple }}" + check_mode: yes + register: result + + - name: Validate Group going to be created + assert: + that: + - result is changed + - admins_group + - devs_group + - '"jane.smith@ansible.org" in {{ admins_group.users }}' + - '"jim.adams@ansible.org" in {{ devs_group.users }}' + - '"jordanbulls@ansible.org" in {{ devs_group.users }}' + - admins_group.users | length == 1 + - devs_group.users | length == 2 + vars: + admins_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'admins') | first }}" + devs_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'developers') | first }}" + + - name: Synchronize Groups - User defined mapping + community.okd.openshift_adm_groups_sync: + config: "{{ configs.user_defined }}" + check_mode: yes + register: result + + - name: Validate Group going to be created + assert: + that: + - result is changed + - admins_group + - devs_group + - '"jane.smith@ansible.org" in {{ admins_group.users }}' + - '"jim.adams@ansible.org" in {{ devs_group.users }}' + - '"jordanbulls@ansible.org" in {{ devs_group.users }}' + - admins_group.users | length == 1 + - devs_group.users | length == 2 + vars: + admins_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'ansible-admins') | first }}" + devs_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'ansible-devs') | first }}" + + - name: Synchronize Groups - Using dn for every query + community.okd.openshift_adm_groups_sync: + config: "{{ configs.dn_everywhere }}" + check_mode: yes + register: result + + - name: Validate Group going to be created + assert: + that: + - result is changed + - admins_group + - devs_group + - '"cn=Jane,ou=people,ou=rfc2307,{{ ldap_root }}" in {{ admins_group.users }}' + - '"cn=Jim,ou=people,ou=rfc2307,{{ ldap_root }}" in {{ devs_group.users }}' + - '"cn=Jordan,ou=people,ou=rfc2307,{{ ldap_root }}" in {{ devs_group.users }}' + - admins_group.users | length == 1 + - devs_group.users | length == 2 + vars: + admins_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'cn=admins,ou=groups,ou=rfc2307,' + ldap_root ) | first }}" + devs_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'cn=developers,ou=groups,ou=rfc2307,' + ldap_root ) | first }}" + + - name: Synchronize Groups - Partially user defined mapping + community.okd.openshift_adm_groups_sync: + config: "{{ configs.partially_user_defined }}" + check_mode: yes + register: result + + - name: Validate Group going to be created + assert: + that: + - result is changed + - admins_group + - devs_group + - '"jane.smith@ansible.org" in {{ admins_group.users }}' + - '"jim.adams@ansible.org" in {{ devs_group.users }}' + - '"jordanbulls@ansible.org" in {{ devs_group.users }}' + - admins_group.users | length == 1 + - devs_group.users | length == 2 + vars: + admins_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'ansible-admins') | first }}" + devs_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'developers') | first }}" + + - name: Delete Group 'engineers' if created before + community.okd.k8s: + state: absent + kind: Group + version: "user.openshift.io/v1" + name: 'engineers' + wait: yes + ignore_errors: yes + + - name: Synchronize Groups - Partially user defined mapping + community.okd.openshift_adm_groups_sync: + config: "{{ configs.out_scope }}" + check_mode: yes + register: result + ignore_errors: yes + + - name: Assert group sync failed due to non-existent member + assert: + that: + - result is failed + - result.msg.startswith("Entry not found for base='cn=Matthew,ou=people,ou=outrfc2307,{{ ldap_root }}'") + + - name: Define sync configuration with tolerateMemberNotFoundErrors + set_fact: + config_out_of_scope_tolerate_not_found: "{{ configs.out_scope | combine({'rfc2307': merge_rfc2307 })}}" + vars: + merge_rfc2307: "{{ configs.out_scope.rfc2307 | combine({'tolerateMemberNotFoundErrors': 'true'}) }}" + + - name: Synchronize Groups - Partially user defined mapping (tolerateMemberNotFoundErrors=true) + community.okd.openshift_adm_groups_sync: + config: "{{ config_out_of_scope_tolerate_not_found }}" + check_mode: yes + register: result + + - name: Assert group sync did not fail (tolerateMemberNotFoundErrors=true) + assert: + that: + - result is changed + - result.groups | length == 1 + - result.groups.0.metadata.name == 'engineers' + - result.groups.0.users == ['Abraham'] + + - name: Create Group 'engineers' + community.okd.k8s: + state: present + wait: yes + definition: + kind: Group + apiVersion: "user.openshift.io/v1" + metadata: + name: engineers + users: [] + + - name: Try to sync LDAP group with Openshift existing group not created using sync should failed + community.okd.openshift_adm_groups_sync: + config: "{{ config_out_of_scope_tolerate_not_found }}" + check_mode: yes + register: result + ignore_errors: yes + + - name: Validate group sync failed + assert: + that: + - result is failed + - '"openshift.io/ldap.host label did not match sync host" in result.msg' + + - name: Define allow_groups and deny_groups groups + set_fact: + allow_groups: + - "cn=developers,ou=groups,ou=rfc2307,{{ ldap_root }}" + deny_groups: + - "cn=admins,ou=groups,ou=rfc2307,{{ ldap_root }}" + + - name: Synchronize Groups using allow_groups + community.okd.openshift_adm_groups_sync: + config: "{{ configs.simple }}" + allow_groups: "{{ allow_groups }}" + register: result + check_mode: yes + + - name: Validate Group going to be created + assert: + that: + - result is changed + - result.groups | length == 1 + - result.groups.0.metadata.name == "developers" + + - name: Synchronize Groups using deny_groups + community.okd.openshift_adm_groups_sync: + config: "{{ configs.simple }}" + deny_groups: "{{ deny_groups }}" + register: result + check_mode: yes + + - name: Validate Group going to be created + assert: + that: + - result is changed + - result.groups | length == 1 + - result.groups.0.metadata.name == "developers" + + - name: Synchronize groups, remove check_mode + community.okd.openshift_adm_groups_sync: + config: "{{ configs.simple }}" + register: result + + - name: Validate result is changed + assert: + that: + - result is changed + + - name: Read Groups + kubernetes.core.k8s_info: + kind: Group + version: "user.openshift.io/v1" + name: admins + register: result + + - name: Validate group was created + assert: + that: + - result.resources | length == 1 + - '"jane.smith@ansible.org" in {{ result.resources.0.users }}' + + - name: Read Groups + kubernetes.core.k8s_info: + kind: Group + version: "user.openshift.io/v1" + name: developers + register: result + + - name: Validate group was created + assert: + that: + - result.resources | length == 1 + - '"jim.adams@ansible.org" in {{ result.resources.0.users }}' + - '"jordanbulls@ansible.org" in {{ result.resources.0.users }}' + + - name: Set users to delete (no admins users anymore and only 1 developer kept) + set_fact: + users_to_delete: + - "cn=Jane,ou=people,ou=rfc2307,{{ ldap_root }}" + - "cn=Jim,ou=people,ou=rfc2307,{{ ldap_root }}" + + - name: Delete users from LDAP servers + openshift_ldap_entry: + bind_dn: "{{ ldap_bind_dn }}" + bind_pw: "{{ ldap_bind_pw }}" + server_uri: "{{ ldap_server_uri }}" + dn: "{{ item }}" + state: absent + with_items: "{{ users_to_delete }}" + + - name: Define sync configuration with tolerateMemberNotFoundErrors + set_fact: + config_simple_tolerate_not_found: "{{ configs.simple | combine({'rfc2307': merge_rfc2307 })}}" + vars: + merge_rfc2307: "{{ configs.simple.rfc2307 | combine({'tolerateMemberNotFoundErrors': 'true'}) }}" + + - name: Synchronize groups once again after users deletion + community.okd.openshift_adm_groups_sync: + config: "{{ config_simple_tolerate_not_found }}" + register: result + + - name: Validate result is changed + assert: + that: + - result is changed + + - name: Read Groups + kubernetes.core.k8s_info: + kind: Group + version: "user.openshift.io/v1" + name: admins + register: result + + - name: Validate admins group does not contains users anymore + assert: + that: + - result.resources | length == 1 + - result.resources.0.users == [] + + - name: Read Groups + kubernetes.core.k8s_info: + kind: Group + version: "user.openshift.io/v1" + name: developers + register: result + + - name: Validate group was created + assert: + that: + - result.resources | length == 1 + - '"jordanbulls@ansible.org" in {{ result.resources.0.users }}' + + - name: Set group to delete + set_fact: + groups_to_delete: + - "cn=developers,ou=groups,ou=rfc2307,{{ ldap_root }}" + + - name: Delete Group from LDAP servers + openshift_ldap_entry: + bind_dn: "{{ ldap_bind_dn }}" + bind_pw: "{{ ldap_bind_pw }}" + server_uri: "{{ ldap_server_uri }}" + dn: "{{ item }}" + state: absent + with_items: "{{ groups_to_delete }}" + + - name: Prune groups + community.okd.openshift_adm_groups_sync: + config: "{{ config_simple_tolerate_not_found }}" + state: absent + register: result + check_mode: yes + + - name: Validate that only developers group is candidate for Prune + assert: + that: + - result is changed + - result.groups | length == 1 + - result.groups.0.metadata.name == "developers" + + - name: Read Group (validate that check_mode did not performed update in the cluster) + kubernetes.core.k8s_info: + kind: Group + version: "user.openshift.io/v1" + name: developers + register: result + + - name: Assert group was found + assert: + that: + - result.resources | length == 1 + + - name: Prune using allow_groups + community.okd.openshift_adm_groups_sync: + config: "{{ config_simple_tolerate_not_found }}" + allow_groups: + - developers + state: absent + register: result + check_mode: yes + + - name: assert developers group was candidate for prune + assert: + that: + - result is changed + - result.groups | length == 1 + - result.groups.0.metadata.name == "developers" + + - name: Prune using deny_groups + community.okd.openshift_adm_groups_sync: + config: "{{ config_simple_tolerate_not_found }}" + deny_groups: + - developers + state: absent + register: result + check_mode: yes + + - name: assert nothing found candidate for prune + assert: + that: + - result is not changed + - result.groups | length == 0 + + - name: Prune groups + community.okd.openshift_adm_groups_sync: + config: "{{ config_simple_tolerate_not_found }}" + state: absent + register: result + + - name: Validate result is changed + assert: + that: + - result is changed + - result.groups | length == 1 + + - name: Get developers group info + kubernetes.core.k8s_info: + kind: Group + version: "user.openshift.io/v1" + name: developers + register: result + + - name: assert group was deleted + assert: + that: + - result.resources | length == 0 + + - name: Get admins group info + kubernetes.core.k8s_info: + kind: Group + version: "user.openshift.io/v1" + name: admins + register: result + + - name: assert group was not deleted + assert: + that: + - result.resources | length == 1 + + - name: Prune groups once again (idempotency) + community.okd.openshift_adm_groups_sync: + config: "{{ config_simple_tolerate_not_found }}" + state: absent + register: result + + - name: Assert nothing changed + assert: + that: + - result is not changed + - result.groups | length == 0 + + always: + - name: Delete openshift groups if existing + community.okd.k8s: + state: absent + kind: Group + version: "user.openshift.io/v1" + name: "{{ item }}" + with_items: + - admins + - engineers + - developers \ No newline at end of file diff --git a/molecule/default/roles/openshift_adm_groups/templates/ad/definition.j2 b/molecule/default/roles/openshift_adm_groups/templates/ad/definition.j2 new file mode 100644 index 0000000..f1cc6d9 --- /dev/null +++ b/molecule/default/roles/openshift_adm_groups/templates/ad/definition.j2 @@ -0,0 +1,39 @@ +units: + - dn: "ou=activeD,{{ ldap_root }}" + class: + - organizationalUnit + attr: + ou: activeD + - dn: "ou=engineers,ou=activeD,{{ ldap_root }}" + class: + - organizationalUnit + attr: + ou: engineers +users: + - dn: cn=Jane,ou=engineers,ou=activeD,{{ ldap_root }} + class: + - inetOrgPerson + attr: + cn: Jane + sn: Smith + displayName: Jane Smith + mail: jane.smith@ansible.org + employeeType: admins + - dn: cn=Jim,ou=engineers,ou=activeD,{{ ldap_root }} + class: + - inetOrgPerson + attr: + cn: Jim + sn: Adams + displayName: Jim Adams + mail: jim.adams@ansible.org + employeeType: admins + - dn: cn=Jordan,ou=engineers,ou=activeD,{{ ldap_root }} + class: + - inetOrgPerson + attr: + cn: Jordan + sn: Bulls + displayName: Jordan Bulls + mail: jordanbulls@ansible.org + employeeType: developers \ No newline at end of file diff --git a/molecule/default/roles/openshift_adm_groups/templates/ad/sync-config.j2 b/molecule/default/roles/openshift_adm_groups/templates/ad/sync-config.j2 new file mode 100644 index 0000000..5599aff --- /dev/null +++ b/molecule/default/roles/openshift_adm_groups/templates/ad/sync-config.j2 @@ -0,0 +1,12 @@ +kind: LDAPSyncConfig +apiVersion: v1 +url: "{{ ldap_server_uri }}" +insecure: true +activeDirectory: + usersQuery: + baseDN: "ou=engineers,ou=activeD,{{ ldap_root }}" + scope: sub + derefAliases: never + filter: (objectclass=inetOrgPerson) + userNameAttributes: [ mail ] + groupMembershipAttributes: [ employeeType ] \ No newline at end of file diff --git a/molecule/default/roles/openshift_adm_groups/templates/augmented-ad/definition.j2 b/molecule/default/roles/openshift_adm_groups/templates/augmented-ad/definition.j2 new file mode 100644 index 0000000..039c308 --- /dev/null +++ b/molecule/default/roles/openshift_adm_groups/templates/augmented-ad/definition.j2 @@ -0,0 +1,59 @@ +units: + - dn: "ou=augmentedAD,{{ ldap_root }}" + class: + - organizationalUnit + attr: + ou: augmentedAD + - dn: "ou=employee,ou=augmentedAD,{{ ldap_root }}" + class: + - organizationalUnit + attr: + ou: employee + - dn: "ou=category,ou=augmentedAD,{{ ldap_root }}" + class: + - organizationalUnit + attr: + ou: category +groups: + - dn: "cn=banking,ou=category,ou=augmentedAD,{{ ldap_root }}" + class: + - groupOfNames + attr: + cn: banking + description: Banking employees + member: + - cn=James,ou=employee,ou=augmentedAD,{{ ldap_root }} + - cn=Gordon,ou=employee,ou=augmentedAD,{{ ldap_root }} + - dn: "cn=insurance,ou=category,ou=augmentedAD,{{ ldap_root }}" + class: + - groupOfNames + attr: + cn: insurance + description: Insurance employees + member: + - cn=Alice,ou=employee,ou=augmentedAD,{{ ldap_root }} +users: + - dn: cn=James,ou=employee,ou=augmentedAD,{{ ldap_root }} + class: + - inetOrgPerson + attr: + cn: James + sn: Allan + mail: james-allan@ansible.org + businessCategory: cn=banking,ou=category,ou=augmentedAD,{{ ldap_root }} + - dn: cn=Gordon,ou=employee,ou=augmentedAD,{{ ldap_root }} + class: + - inetOrgPerson + attr: + cn: Gordon + sn: Kane + mail: gordon-kane@ansible.org + businessCategory: cn=banking,ou=category,ou=augmentedAD,{{ ldap_root }} + - dn: cn=Alice,ou=employee,ou=augmentedAD,{{ ldap_root }} + class: + - inetOrgPerson + attr: + cn: Alice + sn: Courtney + mail: alice-courtney@ansible.org + businessCategory: cn=insurance,ou=category,ou=augmentedAD,{{ ldap_root }} \ No newline at end of file diff --git a/molecule/default/roles/openshift_adm_groups/templates/augmented-ad/sync-config.j2 b/molecule/default/roles/openshift_adm_groups/templates/augmented-ad/sync-config.j2 new file mode 100644 index 0000000..1972044 --- /dev/null +++ b/molecule/default/roles/openshift_adm_groups/templates/augmented-ad/sync-config.j2 @@ -0,0 +1,20 @@ +kind: LDAPSyncConfig +apiVersion: v1 +url: "{{ ldap_server_uri }}" +insecure: true +augmentedActiveDirectory: + groupsQuery: + baseDN: "ou=category,ou=augmentedAD,{{ ldap_root }}" + scope: sub + derefAliases: never + pageSize: 0 + groupUIDAttribute: dn + groupNameAttributes: [ cn ] + usersQuery: + baseDN: "ou=employee,ou=augmentedAD,{{ ldap_root }}" + scope: sub + derefAliases: never + filter: (objectclass=inetOrgPerson) + pageSize: 0 + userNameAttributes: [ mail ] + groupMembershipAttributes: [ businessCategory ] \ No newline at end of file diff --git a/molecule/default/roles/openshift_adm_groups/templates/rfc2307/definition.j2 b/molecule/default/roles/openshift_adm_groups/templates/rfc2307/definition.j2 new file mode 100644 index 0000000..521eaee --- /dev/null +++ b/molecule/default/roles/openshift_adm_groups/templates/rfc2307/definition.j2 @@ -0,0 +1,102 @@ +units: + - dn: "ou=rfc2307,{{ ldap_root }}" + class: + - organizationalUnit + attr: + ou: rfc2307 + - dn: "ou=groups,ou=rfc2307,{{ ldap_root }}" + class: + - organizationalUnit + attr: + ou: groups + - dn: "ou=people,ou=rfc2307,{{ ldap_root }}" + class: + - organizationalUnit + attr: + ou: people + - dn: "ou=outrfc2307,{{ ldap_root }}" + class: + - organizationalUnit + attr: + ou: outrfc2307 + - dn: "ou=groups,ou=outrfc2307,{{ ldap_root }}" + class: + - organizationalUnit + attr: + ou: groups + - dn: "ou=people,ou=outrfc2307,{{ ldap_root }}" + class: + - organizationalUnit + attr: + ou: people +groups: + - dn: "cn=admins,ou=groups,ou=rfc2307,{{ ldap_root }}" + class: + - groupOfNames + attr: + cn: admins + description: System Administrators + member: + - cn=Jane,ou=people,ou=rfc2307,{{ ldap_root }} + - dn: "cn=developers,ou=groups,ou=rfc2307,{{ ldap_root }}" + class: + - groupOfNames + attr: + cn: developers + description: Developers + member: + - cn=Jim,ou=people,ou=rfc2307,{{ ldap_root }} + - cn=Jordan,ou=people,ou=rfc2307,{{ ldap_root }} + - dn: "cn=engineers,ou=groups,ou=outrfc2307,{{ ldap_root }}" + class: + - groupOfNames + attr: + cn: engineers + description: Engineers + member: + - cn=Jim,ou=people,ou=rfc2307,{{ ldap_root }} + - cn=Jordan,ou=people,ou=rfc2307,{{ ldap_root }} + - cn=Julia,ou=people,ou=outrfc2307,{{ ldap_root }} + - cn=Matthew,ou=people,ou=outrfc2307,{{ ldap_root }} +users: + - dn: cn=Jane,ou=people,ou=rfc2307,{{ ldap_root }} + class: + - person + - organizationalPerson + - inetOrgPerson + attr: + cn: Jane + sn: Smith + displayName: Jane Smith + mail: jane.smith@ansible.org + admin: yes + - dn: cn=Jim,ou=people,ou=rfc2307,{{ ldap_root }} + class: + - person + - organizationalPerson + - inetOrgPerson + attr: + cn: Jim + sn: Adams + displayName: Jim Adams + mail: jim.adams@ansible.org + - dn: cn=Jordan,ou=people,ou=rfc2307,{{ ldap_root }} + class: + - person + - organizationalPerson + - inetOrgPerson + attr: + cn: Jordan + sn: Bulls + displayName: Jordan Bulls + mail: jordanbulls@ansible.org + - dn: cn=Julia,ou=people,ou=outrfc2307,{{ ldap_root }} + class: + - person + - organizationalPerson + - inetOrgPerson + attr: + cn: Julia + sn: Abraham + displayName: Julia Abraham + mail: juliaabraham@ansible.org \ No newline at end of file diff --git a/molecule/default/roles/openshift_adm_groups/templates/rfc2307/sync-config.j2 b/molecule/default/roles/openshift_adm_groups/templates/rfc2307/sync-config.j2 new file mode 100644 index 0000000..70198ca --- /dev/null +++ b/molecule/default/roles/openshift_adm_groups/templates/rfc2307/sync-config.j2 @@ -0,0 +1,105 @@ +simple: + kind: LDAPSyncConfig + apiVersion: v1 + url: "{{ ldap_server_uri }}" + insecure: true + rfc2307: + groupsQuery: + baseDN: "ou=groups,ou=rfc2307,{{ ldap_root }}" + scope: sub + derefAliases: never + filter: (objectclass=groupOfNames) + groupUIDAttribute: dn + groupNameAttributes: [ cn ] + groupMembershipAttributes: [ member ] + usersQuery: + baseDN: "ou=people,ou=rfc2307,{{ ldap_root }}" + scope: sub + derefAliases: never + userUIDAttribute: dn + userNameAttributes: [ mail ] +user_defined: + kind: LDAPSyncConfig + apiVersion: v1 + url: "{{ ldap_server_uri }}" + insecure: true + groupUIDNameMapping: + "cn=admins,ou=groups,ou=rfc2307,{{ ldap_root }}": ansible-admins + "cn=developers,ou=groups,ou=rfc2307,{{ ldap_root }}": ansible-devs + rfc2307: + groupsQuery: + baseDN: "ou=groups,ou=rfc2307,{{ ldap_root }}" + scope: sub + derefAliases: never + filter: (objectclass=groupOfNames) + groupUIDAttribute: dn + groupNameAttributes: [ cn ] + groupMembershipAttributes: [ member ] + usersQuery: + baseDN: "ou=people,ou=rfc2307,{{ ldap_root }}" + scope: sub + derefAliases: never + userUIDAttribute: dn + userNameAttributes: [ mail ] +partially_user_defined: + kind: LDAPSyncConfig + apiVersion: v1 + url: "{{ ldap_server_uri }}" + insecure: true + groupUIDNameMapping: + "cn=admins,ou=groups,ou=rfc2307,{{ ldap_root }}": ansible-admins + rfc2307: + groupsQuery: + baseDN: "ou=groups,ou=rfc2307,{{ ldap_root }}" + scope: sub + derefAliases: never + filter: (objectclass=groupOfNames) + groupUIDAttribute: dn + groupNameAttributes: [ cn ] + groupMembershipAttributes: [ member ] + usersQuery: + baseDN: "ou=people,ou=rfc2307,{{ ldap_root }}" + scope: sub + derefAliases: never + userUIDAttribute: dn + userNameAttributes: [ mail ] +dn_everywhere: + kind: LDAPSyncConfig + apiVersion: v1 + url: "{{ ldap_server_uri }}" + insecure: true + rfc2307: + groupsQuery: + baseDN: "ou=groups,ou=rfc2307,{{ ldap_root }}" + scope: sub + derefAliases: never + filter: (objectclass=groupOfNames) + groupUIDAttribute: dn + groupNameAttributes: [ dn ] + groupMembershipAttributes: [ member ] + usersQuery: + baseDN: "ou=people,ou=rfc2307,{{ ldap_root }}" + scope: sub + derefAliases: never + userUIDAttribute: dn + userNameAttributes: [ dn ] +out_scope: + kind: LDAPSyncConfig + apiVersion: v1 + url: "{{ ldap_server_uri }}" + insecure: true + rfc2307: + groupsQuery: + baseDN: "ou=groups,ou=outrfc2307,{{ ldap_root }}" + scope: sub + derefAliases: never + filter: (objectclass=groupOfNames) + groupUIDAttribute: dn + groupNameAttributes: [ cn ] + groupMembershipAttributes: [ member ] + usersQuery: + baseDN: "ou=people,ou=outrfc2307,{{ ldap_root }}" + scope: sub + derefAliases: never + userUIDAttribute: dn + userNameAttributes: [ sn ] \ No newline at end of file diff --git a/molecule/default/verify.yml b/molecule/default/verify.yml index 8ff0329..e6aca2d 100644 --- a/molecule/default/verify.yml +++ b/molecule/default/verify.yml @@ -81,3 +81,6 @@ kind: Namespace name: process-test state: absent + + roles: + - role: openshift_adm_groups diff --git a/plugins/module_utils/openshift_groups.py b/plugins/module_utils/openshift_groups.py new file mode 100644 index 0000000..b77c79d --- /dev/null +++ b/plugins/module_utils/openshift_groups.py @@ -0,0 +1,466 @@ +#!/usr/bin/env python + +# 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 traceback +from datetime import datetime + +from ansible_collections.kubernetes.core.plugins.module_utils.common import (K8sAnsibleMixin, get_api_client) +from ansible.module_utils.parsing.convert_bool import boolean +from ansible.module_utils._text import to_native +from ansible.module_utils.basic import missing_required_lib + +from ansible_collections.community.okd.plugins.module_utils.openshift_ldap import ( + validate_ldap_sync_config, + ldap_split_host_port, + OpenshiftLDAPRFC2307, + OpenshiftLDAPActiveDirectory, + OpenshiftLDAPAugmentedActiveDirectory +) + +try: + import ldap + HAS_PYTHON_LDAP = True +except ImportError as e: + HAS_PYTHON_LDAP = False + PYTHON_LDAP_ERROR = e + +try: + from ansible_collections.kubernetes.core.plugins.module_utils.common import ( + K8sAnsibleMixin, + get_api_client, + ) + HAS_KUBERNETES_COLLECTION = True +except ImportError as e: + HAS_KUBERNETES_COLLECTION = False + k8s_collection_import_exception = e + K8S_COLLECTION_ERROR = traceback.format_exc() + +try: + from kubernetes.dynamic.exceptions import DynamicApiError +except ImportError as e: + pass + + +LDAP_OPENSHIFT_HOST_LABEL = "openshift.io/ldap.host" +LDAP_OPENSHIFT_URL_ANNOTATION = "openshift.io/ldap.url" +LDAP_OPENSHIFT_UID_ANNOTATION = "openshift.io/ldap.uid" +LDAP_OPENSHIFT_SYNCTIME_ANNOTATION = "openshift.io/ldap.sync-time" + + +def connect_to_ldap(module, server_uri, bind_dn=None, bind_pw=None, insecure=True, ca_file=None): + if insecure: + ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) + elif ca_file: + ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, ca_file) + try: + connection = ldap.initialize(server_uri) + connection.set_option(ldap.OPT_REFERRALS, 0) + + connection.simple_bind_s(bind_dn, bind_pw) + return connection + except ldap.LDAPError as e: + module.fail_json(msg="Cannot bind to the LDAP server '{0}' due to: {1}".format(server_uri, e)) + + +def validate_group_annotation(definition, host_ip): + name = definition['metadata']['name'] + # Validate LDAP URL Annotation + annotate_url = definition['metadata'].get('annotations', {}).get(LDAP_OPENSHIFT_URL_ANNOTATION) + if host_ip: + if not annotate_url: + return "group '{0}' marked as having been synced did not have an '{1}' annotation".format(name, LDAP_OPENSHIFT_URL_ANNOTATION) + elif annotate_url != host_ip: + return "group '{0}' was not synchronized from: '{1}'".format(name, host_ip) + # Validate LDAP UID Annotation + annotate_uid = definition['metadata']['annotations'].get(LDAP_OPENSHIFT_UID_ANNOTATION) + if not annotate_uid: + return "group '{0}' marked as having been synced did not have an '{1}' annotation".format(name, LDAP_OPENSHIFT_UID_ANNOTATION) + return None + + +class OpenshiftLDAPGroups(object): + + kind = "Group" + version = "user.openshift.io/v1" + + def __init__(self, module): + self.module = module + self.cache = {} + self.__group_api = None + + @property + def k8s_group_api(self): + if not self.__group_api: + params = dict( + kind=self.kind, + api_version=self.version, + fail=True + ) + self.__group_api = self.module.find_resource(**params) + return self.__group_api + + def get_group_info(self, return_list=False, **kwargs): + params = dict( + kind=self.kind, + api_version=self.version, + ) + params.update(kwargs) + result = self.module.kubernetes_facts(**params) + if len(result["resources"]) == 0: + return None + if len(result["resources"]) == 1 and not return_list: + return result["resources"][0] + else: + return result["resources"] + + def list_groups(self): + allow_groups = self.module.params.get("allow_groups") + deny_groups = self.module.params.get("deny_groups") + name_mapping = self.module.config.get("groupUIDNameMapping") + + if name_mapping and (allow_groups or deny_groups): + + def _map_group_names(groups): + return [name_mapping.get(value, value) for value in groups] + + allow_groups = _map_group_names(allow_groups) + deny_groups = _map_group_names(deny_groups) + + host = self.module.host + netlocation = self.module.netlocation + groups = [] + if allow_groups: + missing = [] + for grp in allow_groups: + if grp in deny_groups: + continue + resource = self.get_group_info(name=grp) + if not resource: + missing.append(grp) + continue + groups.append(resource) + + if missing: + self.module.fail_json( + msg="The following groups were not found: %s" % ''.join(missing) + ) + else: + label_selector = "%s=%s" % (LDAP_OPENSHIFT_HOST_LABEL, host) + resources = self.get_group_info(label_selectors=[label_selector], return_list=True) + if not resources: + return None, "Unable to find Group matching label selector '%s'" % label_selector + groups = resources + if deny_groups: + groups = [item for item in groups if item["metadata"]["name"] not in deny_groups] + + uids = [] + for grp in groups: + err = validate_group_annotation(grp, netlocation) + if err and allow_groups: + # We raise an error for group part of the allow_group not matching LDAP sync criteria + return None, err + group_uid = grp['metadata']['annotations'].get(LDAP_OPENSHIFT_UID_ANNOTATION) + self.cache[group_uid] = grp + uids.append(group_uid) + return uids, None + + def get_group_name_for_uid(self, group_uid): + if group_uid not in self.cache: + return None, "No mapping found for Group uid: %s" % group_uid + return self.cache[group_uid]["metadata"]["name"], None + + def make_openshift_group(self, group_uid, group_name, usernames): + group = self.get_group_info(name=group_name) + if not group: + group = { + "apiVersion": "user.openshift.io/v1", + "kind": "Group", + "metadata": { + "name": group_name, + "labels": { + LDAP_OPENSHIFT_HOST_LABEL: self.module.host + }, + "annotations": { + LDAP_OPENSHIFT_URL_ANNOTATION: self.module.netlocation, + LDAP_OPENSHIFT_UID_ANNOTATION: group_uid, + } + } + } + + # Make sure we aren't taking over an OpenShift group that is already related to a different LDAP group + ldaphost_label = group["metadata"].get("labels", {}).get(LDAP_OPENSHIFT_HOST_LABEL) + if not ldaphost_label or ldaphost_label != self.module.host: + return None, "Group %s: %s label did not match sync host: wanted %s, got %s" % ( + group_name, LDAP_OPENSHIFT_HOST_LABEL, self.module.host, ldaphost_label + ) + + ldapurl_annotation = group["metadata"].get("annotations", {}).get(LDAP_OPENSHIFT_URL_ANNOTATION) + if not ldapurl_annotation or ldapurl_annotation != self.module.netlocation: + return None, "Group %s: %s annotation did not match sync host: wanted %s, got %s" % ( + group_name, LDAP_OPENSHIFT_URL_ANNOTATION, self.module.netlocation, ldapurl_annotation + ) + + ldapuid_annotation = group["metadata"].get("annotations", {}).get(LDAP_OPENSHIFT_UID_ANNOTATION) + if not ldapuid_annotation or ldapuid_annotation != group_uid: + return None, "Group %s: %s annotation did not match LDAP UID: wanted %s, got %s" % ( + group_name, LDAP_OPENSHIFT_UID_ANNOTATION, group_uid, ldapuid_annotation + ) + + # Overwrite Group Users data + group["users"] = usernames + group["metadata"]["annotations"][LDAP_OPENSHIFT_SYNCTIME_ANNOTATION] = datetime.now().isoformat() + return group, None + + def create_openshift_groups(self, groups: list): + diffs = [] + results = [] + changed = False + for definition in groups: + name = definition["metadata"]["name"] + existing = self.get_group_info(name=name) + if not self.module.check_mode: + try: + if existing: + method = 'patch' + definition = self.k8s_group_api.patch(definition).to_dict() + else: + method = 'create' + definition = self.k8s_group_api.create(definition).to_dict() + except DynamicApiError as exc: + self.module.fail_json(msg="Failed to %s Group '%s' due to: %s" % (method, name, exc.body)) + except Exception as exc: + self.module.fail_json(msg="Failed to %s Group '%s' due to: %s" % (method, name, to_native(exc))) + equals = False + if existing: + equals, diff = self.module.diff_objects(existing, definition) + diffs.append(diff) + changed = changed or not equals + results.append(definition) + return results, diffs, changed + + def delete_openshift_group(self, name: str): + result = dict( + kind=self.kind, + apiVersion=self.version, + metadata=dict( + name=name + ) + ) + if not self.module.check_mode: + try: + result = self.k8s_group_api.delete(name=name).to_dict() + except DynamicApiError as exc: + self.module.fail_json(msg="Failed to delete Group '{0}' due to: {1}".format(name, exc.body)) + except Exception as exc: + self.module.fail_json(msg="Failed to delete Group '{0}' due to: {1}".format(name, to_native(exc))) + return result + + +class OpenshiftGroupsSync(K8sAnsibleMixin): + + def __init__(self, module): + + self.module = module + + if not HAS_KUBERNETES_COLLECTION: + self.module.fail_json( + msg="The kubernetes.core collection must be installed", + exception=K8S_COLLECTION_ERROR, + error=to_native(k8s_collection_import_exception), + ) + + if not HAS_PYTHON_LDAP: + self.fail_json( + msg=missing_required_lib('python-ldap'), error=to_native(PYTHON_LDAP_ERROR) + ) + + super(OpenshiftGroupsSync, self).__init__(self.module) + + self.params = self.module.params + self.check_mode = self.module.check_mode + self.client = get_api_client(self.module) + + self.__k8s_group_api = None + self.__ldap_connection = None + self.host = None + self.port = None + self.netlocation = None + self.scheme = None + self.config = self.params.get("sync_config") + + @property + def k8s_group_api(self): + if not self.__k8s_group_api: + params = dict( + kind="Group", + api_version="user.openshift.io/v1", + fail=True + ) + self.__k8s_group_api = self.find_resource(**params) + return self.__k8s_group_api + + @property + def hostIP(self): + return self.netlocation + + @property + def connection(self): + if not self.__ldap_connection: + # Create connection object + params = dict( + module=self, + server_uri=self.config.get('url'), + bind_dn=self.config.get('bindDN'), + bind_pw=self.config.get('bindPassword'), + insecure=boolean(self.config.get('insecure')), + ca_file=self.config.get('ca') + ) + self.__ldap_connection = connect_to_ldap(**params) + return self.__ldap_connection + + def close_connection(self): + if self.__ldap_connection: + self.__ldap_connection.unbind_s() + self.__ldap_connection = None + + def exit_json(self, **kwargs): + self.close_connection() + self.module.exit_json(**kwargs) + + def fail_json(self, **kwargs): + self.close_connection() + self.module.fail_json(**kwargs) + + def get_syncer(self): + syncer = None + if "rfc2307" in self.config: + syncer = OpenshiftLDAPRFC2307(self.config, self.connection) + elif "activeDirectory" in self.config: + syncer = OpenshiftLDAPActiveDirectory(self.config, self.connection) + elif "augmentedActiveDirectory" in self.config: + syncer = OpenshiftLDAPAugmentedActiveDirectory(self.config, self.connection) + else: + msg = "No schema-specific config was found, should be one of 'rfc2307', 'activeDirectory', 'augmentedActiveDirectory'" + self.fail_json(msg=msg) + return syncer + + def synchronize(self): + + sync_group_type = self.module.params.get("type") + + groups_uids = [] + ldap_openshift_group = OpenshiftLDAPGroups(module=self) + + # Get Synchronize object + syncer = self.get_syncer() + + # Determine what to sync : list groups + if sync_group_type == "openshift": + groups_uids, err = ldap_openshift_group.list_groups() + if err: + self.fail_json(msg="Failed to list openshift groups", errors=err) + else: + # List LDAP Group to synchronize + groups_uids = self.params.get("allow_groups") + if not groups_uids: + groups_uids, err = syncer.list_groups() + if err: + self.module.fail_json(msg=err) + deny_groups = self.params.get("deny_groups") + if deny_groups: + groups_uids = [uid for uid in groups_uids if uid not in deny_groups] + + openshift_groups = [] + for uid in groups_uids: + # Get membership data + member_entries, err = syncer.extract_members(uid) + if err: + self.fail_json(msg=err) + + # Determine usernames for members entries + usernames = [] + for entry in member_entries: + name, err = syncer.get_username_for_entry(entry) + if err: + self.exit_json( + msg="Unable to determine username for entry %s: %s" % (entry, err) + ) + if isinstance(name, list): + usernames.extend(name) + else: + usernames.append(name) + # Get group name + if sync_group_type == "openshift": + group_name, err = ldap_openshift_group.get_group_name_for_uid(uid) + else: + group_name, err = syncer.get_group_name_for_uid(uid) + if err: + self.exit_json(msg=err) + + # Make Openshift group + group, err = ldap_openshift_group.make_openshift_group(uid, group_name, usernames) + if err: + self.fail_json(msg=err) + openshift_groups.append(group) + + # Create Openshift Groups + results, diffs, changed = ldap_openshift_group.create_openshift_groups(openshift_groups) + self.module.exit_json(changed=True, groups=results) + + def prune(self): + ldap_openshift_group = OpenshiftLDAPGroups(module=self) + groups_uids, err = ldap_openshift_group.list_groups() + if err: + self.fail_json(msg="Failed to list openshift groups", errors=err) + + # Get Synchronize object + syncer = self.get_syncer() + + changed = False + groups = [] + for uid in groups_uids: + # Check if LDAP group exist + exists, err = syncer.is_ldapgroup_exists(uid) + if err: + msg = "Error determining LDAP group existence for group %s: %s" % (uid, err) + self.module.fail_json(msg=msg) + + if exists: + continue + + # if the LDAP entry that was previously used to create the group doesn't exist, prune it + group_name, err = ldap_openshift_group.get_group_name_for_uid(uid) + if err: + self.module.fail_json(msg=err) + + # Delete Group + result = ldap_openshift_group.delete_openshift_group(group_name) + groups.append(result) + changed = True + + self.exit_json(changed=changed, groups=groups) + + def execute_module(self): + # validate LDAP sync configuration + error = validate_ldap_sync_config(self.config) + if error: + self.fail_json(msg="Invalid LDAP Sync config: %s" % error) + + # Split host/port + if self.config.get('url'): + result, error = ldap_split_host_port(self.config.get('url')) + if error: + self.fail_json(msg="Failed to parse url='{0}': {1}".format(self.config.get('url'), error)) + self.netlocation, self.host, self.port = result["netlocation"], result["host"], result["port"] + self.scheme = result["scheme"] + + if self.params.get('state') == 'present': + self.synchronize() + else: + self.prune() diff --git a/plugins/module_utils/openshift_ldap.py b/plugins/module_utils/openshift_ldap.py new file mode 100644 index 0000000..22be2ec --- /dev/null +++ b/plugins/module_utils/openshift_ldap.py @@ -0,0 +1,777 @@ +#!/usr/bin/env python + +# 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 copy + +from ansible.module_utils.parsing.convert_bool import boolean +from ansible.module_utils.six import iteritems + +try: + import ldap +except ImportError as e: + pass + + +LDAP_SEARCH_OUT_OF_SCOPE_ERROR = "trying to search by DN for an entry that exists outside of the tree specified with the BaseDN for search" + + +def validate_ldap_sync_config(config): + # Validate url + url = config.get('url') + if not url: + return "url should be non empty attribute." + + # Make sure bindDN and bindPassword are both set, or both unset + bind_dn = config.get('bindDN', "") + bind_password = config.get('bindPassword', "") + if (len(bind_dn) == 0) != (len(bind_password) == 0): + return "bindDN and bindPassword must both be specified, or both be empty." + + insecure = boolean(config.get('insecure')) + ca_file = config.get('ca') + if insecure: + if url.startswith('ldaps://'): + return "Cannot use ldaps scheme with insecure=true." + if ca_file: + return "Cannot specify a ca with insecure=true." + elif ca_file and not os.path.isfile(ca_file): + return "could not read ca file: {0}.".format(ca_file) + + nameMapping = config.get('groupUIDNameMapping', {}) + for k, v in iteritems(nameMapping): + if len(k) == 0 or len(v) == 0: + return "groupUIDNameMapping has empty key or value" + + schemas = [] + schema_list = ('rfc2307', 'activeDirectory', 'augmentedActiveDirectory') + for schema in schema_list: + if schema in config: + schemas.append(schema) + + if len(schemas) == 0: + return "No schema-specific config was provided, should be one of %s" % schema_list + if len(schemas) > 1: + return "Exactly one schema-specific config is required; found (%d) %s" % (len(schemas), ','.join(schemas)) + + if schemas[0] == 'rfc2307': + return validate_RFC2307(config.get("rfc2307")) + elif schemas[0] == 'activeDirectory': + return validate_ActiveDirectory(config.get("activeDirectory")) + elif schemas[0] == 'augmentedActiveDirectory': + return validate_AugmentedActiveDirectory(config.get("augmentedActiveDirectory")) + + +def validate_ldap_query(qry, isDNOnly=False): + + # validate query scope + scope = qry.get('scope') + if scope and scope not in ("", "sub", "one", "base"): + return "invalid scope %s" % scope + + # validate deref aliases + derefAlias = qry.get('derefAliases') + if derefAlias and derefAlias not in ("never", "search", "base", "always"): + return "not a valid LDAP alias dereferncing behavior: %s", derefAlias + + # validate timeout + timeout = qry.get('timeout') + if timeout and float(timeout) < 0: + return "timeout must be equal to or greater than zero" + + # Validate DN only + qry_filter = qry.get('filter', "") + if isDNOnly: + if len(qry_filter) > 0: + return 'cannot specify a filter when using "dn" as the UID attribute' + else: + # validate filter + if len(qry_filter) == 0 or qry_filter[0] != '(': + return "filter does not start with an '('" + return None + + +def validate_RFC2307(config): + qry = config.get('groupsQuery') + if not qry or not isinstance(qry, dict): + return "RFC2307: groupsQuery requires a dictionary" + error = validate_ldap_query(qry) + if not error: + return error + for field in ('groupUIDAttribute', 'groupNameAttributes', 'groupMembershipAttributes', + 'userUIDAttribute', 'userNameAttributes'): + value = config.get(field) + if not value: + return "RFC2307: {0} is required.".format(field) + + users_qry = config.get('usersQuery') + if not users_qry or not isinstance(users_qry, dict): + return "RFC2307: usersQuery requires a dictionary" + + isUserDNOnly = (config.get('userUIDAttribute').strip() == 'dn') + return validate_ldap_query(users_qry, isDNOnly=isUserDNOnly) + + +def validate_ActiveDirectory(config, label="ActiveDirectory"): + users_qry = config.get('usersQuery') + if not users_qry or not isinstance(users_qry, dict): + return "{0}: usersQuery requires as dictionnary".format(label) + error = validate_ldap_query(users_qry) + if not error: + return error + + for field in ('userNameAttributes', 'groupMembershipAttributes'): + value = config.get(field) + if not value: + return "{0}: {1} is required.".format(field, label) + + return None + + +def validate_AugmentedActiveDirectory(config): + error = validate_ActiveDirectory(config, label="AugmentedActiveDirectory") + if not error: + return error + for field in ('groupUIDAttribute', 'groupNameAttributes'): + value = config.get(field) + if not value: + return "AugmentedActiveDirectory: {0} is required".format(field) + groups_qry = config.get('groupsQuery') + if not groups_qry or not isinstance(groups_qry, dict): + return "AugmentedActiveDirectory: groupsQuery requires as dictionnary." + + isGroupDNOnly = (config.get('groupUIDAttribute').strip() == 'dn') + return validate_ldap_query(groups_qry, isDNOnly=isGroupDNOnly) + + +def determine_ldap_scope(scope): + if scope in ("", "sub"): + return ldap.SCOPE_SUBTREE + elif scope == 'base': + return ldap.SCOPE_BASE + elif scope == 'one': + return ldap.SCOPE_ONELEVEL + return None + + +def determine_deref_aliases(derefAlias): + mapping = { + "never": ldap.DEREF_NEVER, + "search": ldap.DEREF_SEARCHING, + "base": ldap.DEREF_FINDING, + "always": ldap.DEREF_ALWAYS, + } + result = None + if derefAlias in mapping: + result = mapping.get(derefAlias) + return result + + +def openshift_ldap_build_base_query(config): + qry = {} + if config.get('baseDN'): + qry['base'] = config.get('baseDN') + + scope = determine_ldap_scope(config.get('scope')) + if scope: + qry['scope'] = scope + + pageSize = config.get('pageSize') + if pageSize and int(pageSize) > 0: + qry['sizelimit'] = int(pageSize) + + timeout = config.get('timeout') + if timeout and int(timeout) > 0: + qry['timeout'] = int(timeout) + + filter = config.get('filter') + if filter: + qry['filterstr'] = filter + + derefAlias = determine_deref_aliases(config.get('derefAliases')) + if derefAlias: + qry['derefAlias'] = derefAlias + return qry + + +def openshift_ldap_get_attribute_for_entry(entry, attribute): + attributes = [attribute] + if isinstance(attribute, list): + attributes = attribute + for k in attributes: + if k.lower() == 'dn': + return entry[0] + v = entry[1].get(k, None) + if v: + if isinstance(v, list): + result = [] + for x in v: + if hasattr(x, 'decode'): + result.append(x.decode('utf-8')) + else: + result.append(x) + return result + else: + return v.decode('utf-8') if hasattr(v, 'decode') else v + return "" + + +def ldap_split_host_port(hostport): + """ + ldap_split_host_port splits a network address of the form "host:port", + "host%zone:port", "[host]:port" or "[host%zone]:port" into host or + host%zone and port. + """ + result = dict( + scheme=None, netlocation=None, host=None, port=None + ) + if not hostport: + return result, None + + # Extract Scheme + netlocation = hostport + scheme_l = "://" + if "://" in hostport: + idx = hostport.find(scheme_l) + result["scheme"] = hostport[:idx] + netlocation = hostport[idx + len(scheme_l):] + result["netlocation"] = netlocation + + if netlocation[-1] == ']': + # ipv6 literal (with no port) + result["host"] = netlocation + + v = netlocation.rsplit(":", 1) + if len(v) != 1: + try: + result["port"] = int(v[1]) + except ValueError: + return None, "Invalid value specified for port: %s" % v[1] + result["host"] = v[0] + return result, None + + +def openshift_ldap_query_for_entries(connection, qry, unique_entry=True): + # set deref alias (TODO: need to set a default value to reset for each transaction) + derefAlias = qry.pop('derefAlias', None) + if derefAlias: + ldap.set_option(ldap.OPT_DEREF, derefAlias) + try: + result = connection.search_ext_s(**qry) + if not result or len(result) == 0: + return None, "Entry not found for base='{0}' and filter='{1}'".format(qry['base'], qry['filterstr']) + if len(result) > 1 and unique_entry: + if qry.get('scope') == ldap.SCOPE_BASE: + return None, "multiple entries found matching dn={0}: {1}".format(qry['base'], result) + else: + return None, "multiple entries found matching filter {0}: {1}".format(qry['filterstr'], result) + return result, None + except ldap.NO_SUCH_OBJECT: + return None, "search for entry with base dn='{0}' refers to a non-existent entry".format(qry['base']) + + +def openshift_equal_dn_objects(dn_obj, other_dn_obj): + if len(dn_obj) != len(other_dn_obj): + return False + + for k, v in enumerate(dn_obj): + if len(v) != len(other_dn_obj[k]): + return False + for j, item in enumerate(v): + if not (item == other_dn_obj[k][j]): + return False + return True + + +def openshift_equal_dn(dn, other): + dn_obj = ldap.dn.str2dn(dn) + other_dn_obj = ldap.dn.str2dn(other) + + return openshift_equal_dn_objects(dn_obj, other_dn_obj) + + +def openshift_ancestorof_dn(dn, other): + dn_obj = ldap.dn.str2dn(dn) + other_dn_obj = ldap.dn.str2dn(other) + + if len(dn_obj) >= len(other_dn_obj): + return False + # Take the last attribute from the other DN to compare against + return openshift_equal_dn_objects(dn_obj, other_dn_obj[len(other_dn_obj) - len(dn_obj):]) + + +class OpenshiftLDAPQueryOnAttribute(object): + def __init__(self, qry, attribute): + # qry retrieves entries from an LDAP server + self.qry = copy.deepcopy(qry) + # query_attributes is the attribute for a specific filter that, when conjoined with the common filter, + # retrieves the specific LDAP entry from the LDAP server. (e.g. "cn", when formatted with "aGroupName" + # and conjoined with "objectClass=groupOfNames", becomes (&(objectClass=groupOfNames)(cn=aGroupName))") + self.query_attribute = attribute + + @staticmethod + def escape_filter(buffer): + """ + escapes from the provided LDAP filter string the special + characters in the set '(', ')', '*', \\ and those out of the range 0 < c < 0x80, as defined in RFC4515. + """ + output = [] + hex_string = "0123456789abcdef" + for c in buffer: + if ord(c) > 0x7f or c in ('(', ')', '\\', '*') or c == 0: + first = ord(c) >> 4 + second = ord(c) & 0xf + output += ['\\', hex_string[first], hex_string[second]] + else: + output.append(c) + return ''.join(output) + + def build_request(self, ldapuid, attributes): + params = copy.deepcopy(self.qry) + if self.query_attribute.lower() == 'dn': + if ldapuid: + if not openshift_equal_dn(ldapuid, params['base']) and not openshift_ancestorof_dn(params['base'], ldapuid): + return None, LDAP_SEARCH_OUT_OF_SCOPE_ERROR + params['base'] = ldapuid + params['scope'] = ldap.SCOPE_BASE + # filter that returns all values + params['filterstr'] = "(objectClass=*)" + params['attrlist'] = attributes + else: + # Builds the query containing a filter that conjoins the common filter given + # in the configuration with the specific attribute filter for which the attribute value is given + specificFilter = "%s=%s" % (self.escape_filter(self.query_attribute), self.escape_filter(ldapuid)) + qry_filter = params.get('filterstr', None) + if qry_filter: + params['filterstr'] = "(&%s(%s))" % (qry_filter, specificFilter) + params['attrlist'] = attributes + return params, None + + def ldap_search(self, connection, ldapuid, required_attributes, unique_entry=True): + query, error = self.build_request(ldapuid, required_attributes) + if error: + return None, error + # set deref alias (TODO: need to set a default value to reset for each transaction) + derefAlias = query.pop('derefAlias', None) + if derefAlias: + ldap.set_option(ldap.OPT_DEREF, derefAlias) + + try: + result = connection.search_ext_s(**query) + if not result or len(result) == 0: + return None, "Entry not found for base='{0}' and filter='{1}'".format(query['base'], query['filterstr']) + if unique_entry: + if len(result) > 1: + return None, "Multiple Entries found matching search criteria: %s (%s)" % (query, result) + result = result[0] + return result, None + except ldap.NO_SUCH_OBJECT: + return None, "Entry not found for base='{0}' and filter='{1}'".format(query['base'], query['filterstr']) + except Exception as err: + return None, "Request %s failed due to: %s" % (query, err) + + +class OpenshiftLDAPQuery(object): + def __init__(self, qry): + # Query retrieves entries from an LDAP server + self.qry = qry + + def build_request(self, attributes): + params = copy.deepcopy(self.qry) + params['attrlist'] = attributes + return params + + def ldap_search(self, connection, required_attributes): + query = self.build_request(required_attributes) + # set deref alias (TODO: need to set a default value to reset for each transaction) + derefAlias = query.pop('derefAlias', None) + if derefAlias: + ldap.set_option(ldap.OPT_DEREF, derefAlias) + + try: + result = connection.search_ext_s(**query) + if not result or len(result) == 0: + return None, "Entry not found for base='{0}' and filter='{1}'".format(query['base'], query['filterstr']) + return result, None + except ldap.NO_SUCH_OBJECT: + return None, "search for entry with base dn='{0}' refers to a non-existent entry".format(query['base']) + + +class OpenshiftLDAPInterface(object): + + def __init__(self, connection, groupQuery, groupNameAttributes, groupMembershipAttributes, + userQuery, userNameAttributes, config): + + self.connection = connection + self.groupQuery = copy.deepcopy(groupQuery) + self.groupNameAttributes = groupNameAttributes + self.groupMembershipAttributes = groupMembershipAttributes + self.userQuery = copy.deepcopy(userQuery) + self.userNameAttributes = userNameAttributes + self.config = config + + self.tolerate_not_found = boolean(config.get('tolerateMemberNotFoundErrors', False)) + self.tolerate_out_of_scope = boolean(config.get('tolerateMemberOutOfScopeErrors', False)) + + self.required_group_attributes = [self.groupQuery.query_attribute] + for x in self.groupNameAttributes + self.groupMembershipAttributes: + if x not in self.required_group_attributes: + self.required_group_attributes.append(x) + + self.required_user_attributes = [self.userQuery.query_attribute] + for x in self.userNameAttributes: + if x not in self.required_user_attributes: + self.required_user_attributes.append(x) + + self.cached_groups = {} + self.cached_users = {} + + def get_group_entry(self, uid): + """ + get_group_entry returns an LDAP group entry for the given group UID by searching the internal cache + of the LDAPInterface first, then sending an LDAP query if the cache did not contain the entry. + """ + if uid in self.cached_groups: + return self.cached_groups.get(uid), None + + group, err = self.groupQuery.ldap_search(self.connection, uid, self.required_group_attributes) + if err: + return None, err + self.cached_groups[uid] = group + return group, None + + def get_user_entry(self, uid): + """ + get_user_entry returns an LDAP group entry for the given user UID by searching the internal cache + of the LDAPInterface first, then sending an LDAP query if the cache did not contain the entry. + """ + if uid in self.cached_users: + return self.cached_users.get(uid), None + + entry, err = self.userQuery.ldap_search(self.connection, uid, self.required_user_attributes) + if err: + return None, err + self.cached_users[uid] = entry + return entry, None + + def exists(self, ldapuid): + group, error = self.get_group_entry(ldapuid) + return bool(group), error + + def list_groups(self): + group_qry = copy.deepcopy(self.groupQuery.qry) + group_qry['attrlist'] = self.required_group_attributes + + groups, err = openshift_ldap_query_for_entries( + connection=self.connection, + qry=group_qry, + unique_entry=False + ) + if err: + return None, err + + group_uids = [] + for entry in groups: + uid = openshift_ldap_get_attribute_for_entry(entry, self.groupQuery.query_attribute) + if not uid: + return None, "Unable to find LDAP group uid for entry %s" % entry + self.cached_groups[uid] = entry + group_uids.append(uid) + return group_uids, None + + def extract_members(self, uid): + """ + returns the LDAP member entries for a group specified with a ldapGroupUID + """ + # Get group entry from LDAP + group, err = self.get_group_entry(uid) + if err: + return None, err + + # Extract member UIDs from group entry + member_uids = [] + for attribute in self.groupMembershipAttributes: + member_uids += openshift_ldap_get_attribute_for_entry(group, attribute) + + members = [] + for user_uid in member_uids: + entry, err = self.get_user_entry(user_uid) + if err: + if self.tolerate_not_found and err.startswith("Entry not found"): + continue + elif err == LDAP_SEARCH_OUT_OF_SCOPE_ERROR: + continue + return None, err + members.append(entry) + + return members, None + + +class OpenshiftLDAPRFC2307(object): + + def __init__(self, config, ldap_connection): + + self.config = config + self.ldap_interface = self.create_ldap_interface(ldap_connection) + + def create_ldap_interface(self, connection): + segment = self.config.get("rfc2307") + groups_base_qry = openshift_ldap_build_base_query(segment['groupsQuery']) + users_base_qry = openshift_ldap_build_base_query(segment['usersQuery']) + + groups_query = OpenshiftLDAPQueryOnAttribute(groups_base_qry, segment['groupUIDAttribute']) + users_query = OpenshiftLDAPQueryOnAttribute(users_base_qry, segment['userUIDAttribute']) + + params = dict( + connection=connection, + groupQuery=groups_query, + groupNameAttributes=segment['groupNameAttributes'], + groupMembershipAttributes=segment['groupMembershipAttributes'], + userQuery=users_query, + userNameAttributes=segment['userNameAttributes'], + config=segment + ) + return OpenshiftLDAPInterface(**params) + + def get_username_for_entry(self, entry): + username = openshift_ldap_get_attribute_for_entry(entry, self.ldap_interface.userNameAttributes) + if not username: + return None, "The user entry (%s) does not map to a OpenShift User name with the given mapping" % entry + return username, None + + def get_group_name_for_uid(self, uid): + + # Get name from User defined mapping + groupuid_name_mapping = self.config.get("groupUIDNameMapping") + if groupuid_name_mapping and uid in groupuid_name_mapping: + return groupuid_name_mapping.get(uid), None + elif self.ldap_interface.groupNameAttributes: + group, err = self.ldap_interface.get_group_entry(uid) + if err: + return None, err + group_name = openshift_ldap_get_attribute_for_entry(group, self.ldap_interface.groupNameAttributes) + if not group_name: + error = "The group entry (%s) does not map to an OpenShift Group name with the given name attribute (%s)" % ( + group, self.ldap_interface.groupNameAttributes + ) + return None, error + if isinstance(group_name, list): + group_name = group_name[0] + return group_name, None + else: + return None, "No OpenShift Group name defined for LDAP group UID: %s" % uid + + def is_ldapgroup_exists(self, uid): + group, err = self.ldap_interface.get_group_entry(uid) + if err: + if err == LDAP_SEARCH_OUT_OF_SCOPE_ERROR or err.startswith("Entry not found") or "non-existent entry" in err: + return False, None + return False, err + if group: + return True, None + return False, None + + def list_groups(self): + return self.ldap_interface.list_groups() + + def extract_members(self, uid): + return self.ldap_interface.extract_members(uid) + + +class OpenshiftLDAP_ADInterface(object): + + def __init__(self, connection, user_query, group_member_attr, user_name_attr): + self.connection = connection + self.userQuery = user_query + self.groupMembershipAttributes = group_member_attr + self.userNameAttributes = user_name_attr + + self.required_user_attributes = self.userNameAttributes or [] + for attr in self.groupMembershipAttributes: + if attr not in self.required_user_attributes: + self.required_user_attributes.append(attr) + + self.cache = {} + self.cache_populated = False + + def is_entry_present(self, cache_item, entry): + for item in cache_item: + if item[0] == entry[0]: + return True + return False + + def populate_cache(self): + if not self.cache_populated: + self.cache_populated = True + entries, err = self.userQuery.ldap_search(self.connection, self.required_user_attributes) + if err: + return err + + for entry in entries: + for group_attr in self.groupMembershipAttributes: + uids = openshift_ldap_get_attribute_for_entry(entry, group_attr) + if not isinstance(uids, list): + uids = [uids] + for uid in uids: + if uid not in self.cache: + self.cache[uid] = [] + if not self.is_entry_present(self.cache[uid], entry): + self.cache[uid].append(entry) + return None + + def list_groups(self): + err = self.populate_cache() + if err: + return None, err + result = [] + if self.cache: + result = self.cache.keys() + return result, None + + def extract_members(self, uid): + # ExtractMembers returns the LDAP member entries for a group specified with a ldapGroupUID + # if we already have it cached, return the cached value + if uid in self.cache: + return self.cache[uid], None + + # This happens in cases where we did not list out every group. + # In that case, we're going to be asked about specific groups. + users_in_group = [] + for attr in self.groupMembershipAttributes: + query_on_attribute = OpenshiftLDAPQueryOnAttribute(self.userQuery.qry, attr) + entries, error = query_on_attribute.ldap_search(self.connection, uid, self.required_user_attributes, unique_entry=False) + if error and "not found" not in error: + return None, error + if not entries: + continue + + for entry in entries: + if not self.is_entry_present(users_in_group, entry): + users_in_group.append(entry) + + self.cache[uid] = users_in_group + return users_in_group, None + + +class OpenshiftLDAPActiveDirectory(object): + + def __init__(self, config, ldap_connection): + + self.config = config + self.ldap_interface = self.create_ldap_interface(ldap_connection) + + def create_ldap_interface(self, connection): + segment = self.config.get("activeDirectory") + base_query = openshift_ldap_build_base_query(segment['usersQuery']) + user_query = OpenshiftLDAPQuery(base_query) + + return OpenshiftLDAP_ADInterface( + connection=connection, + user_query=user_query, + group_member_attr=segment["groupMembershipAttributes"], + user_name_attr=segment["userNameAttributes"], + ) + + def get_username_for_entry(self, entry): + username = openshift_ldap_get_attribute_for_entry(entry, self.ldap_interface.userNameAttributes) + if not username: + return None, "The user entry (%s) does not map to a OpenShift User name with the given mapping" % entry + return username, None + + def get_group_name_for_uid(self, uid): + return uid, None + + def is_ldapgroup_exists(self, uid): + members, error = self.extract_members(uid) + if error: + return False, error + exists = members and len(members) > 0 + return exists, None + + def list_groups(self): + return self.ldap_interface.list_groups() + + def extract_members(self, uid): + return self.ldap_interface.extract_members(uid) + + +class OpenshiftLDAP_AugmentedADInterface(OpenshiftLDAP_ADInterface): + + def __init__(self, connection, user_query, group_member_attr, user_name_attr, group_qry, group_name_attr): + super(OpenshiftLDAP_AugmentedADInterface, self).__init__( + connection, user_query, group_member_attr, user_name_attr + ) + self.groupQuery = copy.deepcopy(group_qry) + self.groupNameAttributes = group_name_attr + + self.required_group_attributes = [self.groupQuery.query_attribute] + for x in self.groupNameAttributes: + if x not in self.required_group_attributes: + self.required_group_attributes.append(x) + + self.cached_groups = {} + + def get_group_entry(self, uid): + """ + get_group_entry returns an LDAP group entry for the given group UID by searching the internal cache + of the LDAPInterface first, then sending an LDAP query if the cache did not contain the entry. + """ + if uid in self.cached_groups: + return self.cached_groups.get(uid), None + + group, err = self.groupQuery.ldap_search(self.connection, uid, self.required_group_attributes) + if err: + return None, err + self.cached_groups[uid] = group + return group, None + + def exists(self, ldapuid): + # Get group members + members, error = self.extract_members(ldapuid) + if error: + return False, error + group_exists = bool(members) + + # Check group Existence + entry, error = self.get_group_entry(ldapuid) + if error: + if "not found" in error: + return False, None + else: + return False, error + else: + return group_exists and bool(entry), None + + +class OpenshiftLDAPAugmentedActiveDirectory(OpenshiftLDAPRFC2307): + + def __init__(self, config, ldap_connection): + + self.config = config + self.ldap_interface = self.create_ldap_interface(ldap_connection) + + def create_ldap_interface(self, connection): + segment = self.config.get("augmentedActiveDirectory") + user_base_query = openshift_ldap_build_base_query(segment['usersQuery']) + groups_base_qry = openshift_ldap_build_base_query(segment['groupsQuery']) + + user_query = OpenshiftLDAPQuery(user_base_query) + groups_query = OpenshiftLDAPQueryOnAttribute(groups_base_qry, segment['groupUIDAttribute']) + + return OpenshiftLDAP_AugmentedADInterface( + connection=connection, + user_query=user_query, + group_member_attr=segment["groupMembershipAttributes"], + user_name_attr=segment["userNameAttributes"], + group_qry=groups_query, + group_name_attr=segment["groupNameAttributes"] + ) + + def is_ldapgroup_exists(self, uid): + return self.ldap_interface.exists(uid) diff --git a/plugins/modules/openshift_adm_groups_sync.py b/plugins/modules/openshift_adm_groups_sync.py new file mode 100644 index 0000000..3b3aff2 --- /dev/null +++ b/plugins/modules/openshift_adm_groups_sync.py @@ -0,0 +1,226 @@ +#!/usr/bin/python + +# 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 + +DOCUMENTATION = r""" + +module: openshift_adm_groups_sync + +short_description: Sync OpenShift Groups with records from an external provider. + +version_added: "2.1.0" + +author: + - Aubin Bikouo (@abikouo) + +description: + - In order to sync/prune OpenShift Group records with those from an external provider, determine which Groups you wish to sync + and where their records live. + - Analogous to `oc adm prune groups` and `oc adm group sync`. + - LDAP sync configuration file syntax can be found here + U(https://docs.openshift.com/container-platform/4.9/authentication/ldap-syncing.html). + - The bindPassword attribute of the LDAP sync configuration is expected to be a string, + please use ansible-vault encryption to secure this information. + +extends_documentation_fragment: + - kubernetes.core.k8s_auth_options + +options: + state: + description: + - Determines if the group should be sync when set to C(present) or pruned when set to C(absent). + type: str + default: present + choices: [ absent, present ] + type: + description: + - which groups allow and deny list entries refer to. + type: str + default: ldap + choices: [ ldap, openshift ] + sync_config: + description: + - Provide a valid YAML definition of an LDAP sync configuration. + type: dict + aliases: + - config + - src + required: True + deny_groups: + description: + - Denied groups, could be openshift group name or LDAP group dn value. + - When parameter C(type) is set to I(ldap) this should contains only LDAP group definition + like I(cn=developers,ou=groups,ou=rfc2307,dc=ansible,dc=redhat). + - The elements specified in this list will override the ones specified in C(allow_groups). + type: list + elements: str + allow_groups: + description: + - Allowed groups, could be openshift group name or LDAP group dn value. + - When parameter C(type) is set to I(ldap) this should contains only LDAP group definition + like I(cn=developers,ou=groups,ou=rfc2307,dc=ansible,dc=redhat). + type: list + elements: str + +requirements: + - python >= 3.6 + - kubernetes >= 12.0.0 + - python-ldap +""" + +EXAMPLES = r""" +# Prune all orphaned groups +- name: Prune all orphan groups + openshift_adm_groups_sync: + state: absent + src: "{{ lookup('file', '/path/to/ldap-sync-config.yaml') | from_yaml }}" + +# Prune all orphaned groups from a list of specific groups specified in allow_groups +- name: Prune all orphan groups from a list of specific groups specified in allow_groups + openshift_adm_groups_sync: + state: absent + src: "{{ lookup('file', '/path/to/ldap-sync-config.yaml') | from_yaml }}" + allow_groups: + - cn=developers,ou=groups,ou=rfc2307,dc=ansible,dc=redhat + - cn=developers,ou=groups,ou=rfc2307,dc=ansible,dc=redhat + +# Sync all groups from an LDAP server +- name: Sync all groups from an LDAP server + openshift_adm_groups_sync: + src: + kind: LDAPSyncConfig + apiVersion: v1 + url: ldap://localhost:1390 + insecure: true + bindDN: cn=admin,dc=example,dc=org + bindPassword: adminpassword + rfc2307: + groupsQuery: + baseDN: "cn=admins,ou=groups,dc=example,dc=org" + scope: sub + derefAliases: never + filter: (objectClass=*) + pageSize: 0 + groupUIDAttribute: dn + groupNameAttributes: [ cn ] + groupMembershipAttributes: [ member ] + usersQuery: + baseDN: "ou=users,dc=example,dc=org" + scope: sub + derefAliases: never + pageSize: 0 + userUIDAttribute: dn + userNameAttributes: [ mail ] + tolerateMemberNotFoundErrors: true + tolerateMemberOutOfScopeErrors: true + +# Sync all groups except the ones from the deny_groups from an LDAP server +- name: Sync all groups from an LDAP server using deny_groups + openshift_adm_groups_sync: + src: "{{ lookup('file', '/path/to/ldap-sync-config.yaml') | from_yaml }}" + deny_groups: + - cn=developers,ou=groups,ou=rfc2307,dc=ansible,dc=redhat + - cn=developers,ou=groups,ou=rfc2307,dc=ansible,dc=redhat + +# Sync all OpenShift Groups that have been synced previously with an LDAP server +- name: Sync all OpenShift Groups that have been synced previously with an LDAP server + openshift_adm_groups_sync: + src: "{{ lookup('file', '/path/to/ldap-sync-config.yaml') | from_yaml }}" + type: openshift +""" + + +RETURN = r""" +builds: + description: + - The groups that were created, updated or deleted + returned: success + type: list + elements: dict + sample: [ + { + "apiVersion": "user.openshift.io/v1", + "kind": "Group", + "metadata": { + "annotations": { + "openshift.io/ldap.sync-time": "2021-12-17T12:20:28.125282", + "openshift.io/ldap.uid": "cn=developers,ou=groups,ou=rfc2307,dc=ansible,dc=redhat", + "openshift.io/ldap.url": "localhost:1390" + }, + "creationTimestamp": "2021-12-17T11:09:49Z", + "labels": { + "openshift.io/ldap.host": "localhost" + }, + "managedFields": [{ + "apiVersion": "user.openshift.io/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:openshift.io/ldap.sync-time": {}, + "f:openshift.io/ldap.uid": {}, + "f:openshift.io/ldap.url": {} + }, + "f:labels": { + ".": {}, + "f:openshift.io/ldap.host": {} + } + }, + "f:users": {} + }, + "manager": "OpenAPI-Generator", + "operation": "Update", + "time": "2021-12-17T11:09:49Z" + }], + "name": "developers", + "resourceVersion": "2014696", + "uid": "8dc211cb-1544-41e1-96b1-efffeed2d7d7" + }, + "users": ["jordanbulls@ansible.org"] + } + ] +""" + +import copy +import traceback + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.kubernetes.core.plugins.module_utils.args_common import AUTH_ARG_SPEC + + +def argument_spec(): + args = copy.deepcopy(AUTH_ARG_SPEC) + args.update( + dict( + state=dict(type='str', choices=['absent', 'present'], default='present'), + type=dict(type='str', choices=['ldap', 'openshift'], default='ldap'), + sync_config=dict(type='dict', aliases=['config', 'src'], required=True), + deny_groups=dict(type='list', elements='str', default=[]), + allow_groups=dict(type='list', elements='str', default=[]), + ) + ) + return args + + +def main(): + module = AnsibleModule(argument_spec=argument_spec(), supports_check_mode=True) + + from ansible_collections.community.okd.plugins.module_utils.openshift_groups import ( + OpenshiftGroupsSync + ) + + try: + openshift_groups = OpenshiftGroupsSync(module) + openshift_groups.execute_module() + except Exception as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + + +if __name__ == '__main__': + main() diff --git a/tests/unit/plugins/module_utils/test_ldap_dn.py b/tests/unit/plugins/module_utils/test_ldap_dn.py new file mode 100644 index 0000000..5835f36 --- /dev/null +++ b/tests/unit/plugins/module_utils/test_ldap_dn.py @@ -0,0 +1,32 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +from ansible_collections.community.okd.plugins.module_utils.openshift_ldap import ( + openshift_equal_dn, + openshift_ancestorof_dn +) +import pytest + +try: + import ldap +except ImportError: + pytestmark = pytest.mark.skip("This test requires the python-ldap library") + + +def test_equal_dn(): + + assert openshift_equal_dn("cn=unit,ou=users,dc=ansible,dc=com", "cn=unit,ou=users,dc=ansible,dc=com") + assert not openshift_equal_dn("cn=unit,ou=users,dc=ansible,dc=com", "cn=units,ou=users,dc=ansible,dc=com") + assert not openshift_equal_dn("cn=unit,ou=users,dc=ansible,dc=com", "cn=unit,ou=user,dc=ansible,dc=com") + assert not openshift_equal_dn("cn=unit,ou=users,dc=ansible,dc=com", "cn=unit,ou=users,dc=ansible,dc=org") + + +def test_ancestor_of_dn(): + + assert not openshift_ancestorof_dn("cn=unit,ou=users,dc=ansible,dc=com", "cn=unit,ou=users,dc=ansible,dc=com") + assert not openshift_ancestorof_dn("cn=unit,ou=users,dc=ansible,dc=com", "cn=units,ou=users,dc=ansible,dc=com") + assert openshift_ancestorof_dn("ou=users,dc=ansible,dc=com", "cn=john,ou=users,dc=ansible,dc=com") + assert openshift_ancestorof_dn("ou=users,dc=ansible,dc=com", "cn=mathew,ou=users,dc=ansible,dc=com") + assert not openshift_ancestorof_dn("ou=users,dc=ansible,dc=com", "cn=mathew,ou=users,dc=ansible,dc=org") diff --git a/tests/unit/plugins/module_utils/test_ldap_sync_config.py b/tests/unit/plugins/module_utils/test_ldap_sync_config.py new file mode 100644 index 0000000..1a8ef67 --- /dev/null +++ b/tests/unit/plugins/module_utils/test_ldap_sync_config.py @@ -0,0 +1,63 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +from ansible_collections.community.okd.plugins.module_utils.openshift_ldap import ( + validate_ldap_sync_config, +) + + +def test_missing_url(): + config = dict( + kind="LDAPSyncConfig", + apiVersion="v1", + insecure=True + ) + err = validate_ldap_sync_config(config) + assert err == "url should be non empty attribute." + + +def test_binddn_and_bindpwd_linked(): + """ + one of bind_dn and bind_pwd cannot be set alone + """ + config = dict( + kind="LDAPSyncConfig", + apiVersion="v1", + url="ldap://LDAP_SERVICE_IP:389", + insecure=True, + bindDN="cn=admin,dc=example,dc=org" + ) + + credentials_error = "bindDN and bindPassword must both be specified, or both be empty." + + assert validate_ldap_sync_config(config) == credentials_error + + config = dict( + kind="LDAPSyncConfig", + apiVersion="v1", + url="ldap://LDAP_SERVICE_IP:389", + insecure=True, + bindPassword="testing1223" + ) + + assert validate_ldap_sync_config(config) == credentials_error + + +def test_insecure_connection(): + config = dict( + kind="LDAPSyncConfig", + apiVersion="v1", + url="ldaps://LDAP_SERVICE_IP:389", + insecure=True, + ) + + assert validate_ldap_sync_config(config) == "Cannot use ldaps scheme with insecure=true." + + config.update(dict( + url="ldap://LDAP_SERVICE_IP:389", + ca="path/to/ca/file" + )) + + assert validate_ldap_sync_config(config) == "Cannot specify a ca with insecure=true."