Repository URL to install this package:
Version:
6.0.0 ▾
|
#!/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)