mirror of
https://github.com/openshift/community.okd.git
synced 2026-03-26 19:03:14 +00:00
openshift adm group sync/prune (#125)
This commit is contained in:
466
plugins/module_utils/openshift_groups.py
Normal file
466
plugins/module_utils/openshift_groups.py
Normal 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()
|
||||
Reference in New Issue
Block a user