Files
community.okd/plugins/module_utils/openshift_groups.py
Bikouo Aubin a63e5b7b36 Update CI - Continue work from #195 (#202)
* Upgrade Ansible and OKD versions for CI

* Use ubi9 and fix sanity

* Use correct pip install

* Try using quotes

* Ensure python3.9

* Upgrade ansible and molecule versions

* Remove DeploymentConfig

DeploymentConfigs are deprecated and seem to now be causing idempotence
problems. Replacing them with Deployments fixes it.

* Attempt to fix ldap integration tests

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Move sanity and unit tests to GH actions

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Firt round of sanity fixes

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Add kubernetes.core collection as sanity requirement

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Add ignore-2.16.txt

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Attempt to fix units

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Add ignore-2.17

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Attempt to fix unit tests

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Add pytest-ansible to test-requirements.txt

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Add changelog fragment

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Add workflow for ansible-lint

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Apply black

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Fix linters

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Add # fmt: skip

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Yet another round of linting

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Yet another round of linting

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Remove setup.cfg

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Revert #fmt

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Use ansible-core 2.14

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Cleanup ansible-lint ignores

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>

* Try using service instead of pod IP

* Fix typo

* Actually use the correct port

* See if NetworkPolicy is preventing connection

* using Pod internal IP

* fix adm prune auth roles syntax

* adding some retry steps

* fix: openshift_builds target

* add flag --force-with-deps when building downstream collection

* Remove yamllint from tox linters, bump minimum python supported version to 3.9, Remove support for ansible-core < 2.14

---------

Signed-off-by: Alina Buzachis <abuzachis@redhat.com>
Co-authored-by: Mike Graves <mgraves@redhat.com>
Co-authored-by: Alina Buzachis <abuzachis@redhat.com>
2023-11-15 17:00:38 +00:00

510 lines
18 KiB
Python

#!/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
from datetime import datetime
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
PYTHON_LDAP_ERROR = None
except ImportError as e:
HAS_PYTHON_LDAP = False
PYTHON_LDAP_ERROR = e
from ansible_collections.community.okd.plugins.module_utils.openshift_common import (
AnsibleOpenshiftModule,
)
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:
method = "patch" if existing else "create"
try:
if existing:
definition = self.k8s_group_api.patch(definition).to_dict()
else:
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(AnsibleOpenshiftModule):
def __init__(self, **kwargs):
super(OpenshiftGroupsSync, self).__init__(**kwargs)
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")
if not HAS_PYTHON_LDAP:
self.fail_json(
msg=missing_required_lib("python-ldap"),
error=to_native(PYTHON_LDAP_ERROR),
)
@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()