openshift adm group sync/prune (#125)

This commit is contained in:
abikouo
2022-01-20 17:23:30 +01:00
committed by GitHub
parent eb11821b3c
commit 0a1a647e37
23 changed files with 3150 additions and 0 deletions

View File

@@ -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 \

View File

@@ -7,3 +7,5 @@ plugin_routing:
action:
k8s:
redirect: kubernetes.core.k8s_info
openshift_adm_groups_sync:
redirect: kubernetes.core.k8s_info

View File

@@ -12,6 +12,7 @@
name:
- kubernetes>=12.0.0
- coverage
- python-ldap
virtualenv: "{{ virtualenv }}"
virtualenv_command: "{{ virtualenv_command }}"
virtualenv_site_packages: no

View File

@@ -0,0 +1,4 @@
---
ldap_admin_user: "admin"
ldap_admin_password: "testing123!"
ldap_root: "dc=ansible,dc=redhat"

View File

@@ -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()

View File

@@ -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()

View File

@@ -0,0 +1,4 @@
---
collections:
- community.okd
- kubernetes.core

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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 ]

View File

@@ -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 }}

View File

@@ -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 ]

View File

@@ -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

View File

@@ -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 ]

View File

@@ -81,3 +81,6 @@
kind: Namespace
name: process-test
state: absent
roles:
- role: openshift_adm_groups

View File

@@ -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()

View File

@@ -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)

View File

@@ -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()

View File

@@ -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")

View File

@@ -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."