Andrew Bogott has submitted this change and it was merged. ( https://gerrit.wikimedia.org/r/338918 )
Change subject: Keystonehooks: Sync ldap project groups with keystone project membership ...................................................................... Keystonehooks: Sync ldap project groups with keystone project membership Bug: T150091 Change-Id: Ibde67f8dd6c5a4d71a781730e9283b164a3544e4 --- A modules/openstack/files/liberty/keystone/wmfkeystonehooks/ldapgroups.py M modules/openstack/files/liberty/keystone/wmfkeystonehooks/wmfkeystonehooks.py 2 files changed, 220 insertions(+), 8 deletions(-) Approvals: Andrew Bogott: Looks good to me, approved jenkins-bot: Verified diff --git a/modules/openstack/files/liberty/keystone/wmfkeystonehooks/ldapgroups.py b/modules/openstack/files/liberty/keystone/wmfkeystonehooks/ldapgroups.py new file mode 100644 index 0000000..8f4bced --- /dev/null +++ b/modules/openstack/files/liberty/keystone/wmfkeystonehooks/ldapgroups.py @@ -0,0 +1,169 @@ +# Copyright 2016 Andrew Bogott for the Wikimedia Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import ldap +import ldap.modlist + +from keystone import exception + +from oslo_log import log as logging +from oslo_config import cfg + +LOG = logging.getLogger('nova.%s' % __name__) + + +def _getLdapInfo(attr, conffile="/etc/ldap.conf"): + try: + f = open(conffile) + except IOError: + if conffile == "/etc/ldap.conf": + # fallback to /etc/ldap/ldap.conf, which will likely + # have less information + f = open("/etc/ldap/ldap.conf") + for line in f: + if line.strip() == "": + continue + if line.split()[0].lower() == attr.lower(): + return line.split(None, 1)[1].strip() + break + + +def _open_ldap(): + ldapHost = _getLdapInfo("uri") + sslType = _getLdapInfo("ssl") + + binddn = cfg.CONF.ldap.user + bindpw = cfg.CONF.ldap.password + ds = ldap.initialize(ldapHost) + ds.protocol_version = ldap.VERSION3 + if sslType == "start_tls": + ds.start_tls_s() + + try: + ds.simple_bind_s(binddn, bindpw) + return ds + except ldap.CONSTRAINT_VIOLATION: + LOG.debug("LDAP bind failure: Too many failed attempts.\n") + except ldap.INVALID_DN_SYNTAX: + LOG.debug("LDAP bind failure: The bind DN is incorrect... \n") + except ldap.NO_SUCH_OBJECT: + LOG.debug("LDAP bind failure: " + "Unable to locate the bind DN account.\n") + except ldap.UNWILLING_TO_PERFORM as msg: + LOG.debug("LDAP bind failure: " + "The LDAP server was unwilling to perform the action" + " requested.\nError was: %s\n" % msg[0]["info"]) + except ldap.INVALID_CREDENTIALS: + LOG.debug("LDAP bind failure: Password incorrect.\n") + + return None + + +# ds is presumed to be an already-open ldap connection +def _all_groups(ds): + basedn = cfg.CONF.wmfhooks.ldap_group_base_dn + allgroups = ds.search_s(basedn, ldap.SCOPE_ONELEVEL) + return allgroups + + +# ds is presumed to be an already-open ldap connection +def _get_next_gid_number(ds): + highest = cfg.CONF.wmfhooks.minimum_gid_number + for group in _all_groups(ds): + if 'gidNumber' in group[1]: + number = int(group[1]['gidNumber'][0]) + if number > highest: + highest = number + + # Fixme: Check against a hard max gid number limit? + return highest + 1 + + +# ds should be an already-open ldap connection. +# +# groupname is the name of the group to create, probably project-<projectname> +def _get_ldap_group(ds, groupname): + basedn = cfg.CONF.wmfhooks.ldap_group_base_dn + searchdn = "cn=%s,%s" % (groupname, basedn) + try: + thisgroup = ds.search_s(searchdn, ldap.SCOPE_BASE) + return thisgroup + except ldap.LDAPError: + return None + + +def delete_ldap_project_group(project_id): + basedn = cfg.CONF.wmfhooks.ldap_group_base_dn + groupname = "project-%s" % project_id.encode('utf-8') + dn = "cn=%s,%s" % (groupname, basedn) + + ds = _open_ldap() + if not ds: + LOG.error("Failed to connect to ldap; Leak a project group.") + raise exception.ValidationError() + + ds.delete_s(dn) + + +def sync_ldap_project_group(project_id, keystone_assignments): + groupname = "project-%s" % project_id.encode('utf-8') + LOG.info("Syncing keystone project membership with ldap group %s" + % groupname) + ds = _open_ldap() + if not ds: + LOG.error("Failed to connect to ldap; cannot set up new project.") + raise exception.ValidationError() + + allusers = set() + for key in keystone_assignments: + allusers |= set(keystone_assignments[key]) + + if 'novaobserver' in allusers: + allusers.remove('novaobserver') + + basedn = cfg.CONF.wmfhooks.ldap_user_base_dn + members = ["uid=%s,%s" % (user.encode('utf-8'), basedn) + for user in allusers] + + basedn = cfg.CONF.wmfhooks.ldap_group_base_dn + dn = "cn=%s,%s" % (groupname, basedn) + + existingEntry = _get_ldap_group(ds, groupname) + if existingEntry: + # We're modifying an existing group + oldEntry = existingEntry[0][1] + newEntry = oldEntry.copy() + newEntry['member'] = members + + modlist = ldap.modlist.modifyModlist(oldEntry, newEntry) + if modlist: + ds.modify_s(dn, modlist) + else: + # We're creating a new group from scratch. + # There is a potential race between _get_next_git_number() + # and ds.add_s, so we make a few attempts. + # around this function. + groupEntry = {} + groupEntry['member'] = members + groupEntry['objectClass'] = ['groupOfNames', 'posixGroup', 'top'] + groupEntry['cn'] = [groupname] + for i in range(0, 4): + groupEntry['gidNumber'] = [str(_get_next_gid_number(ds))] + modlist = ldap.modlist.addModlist(groupEntry) + try: + ds.add_s(dn, modlist) + break + except ldap.LDAPError: + LOG.warning("Failed to create group, attempt number %s: %s" % + (i, modlist)) diff --git a/modules/openstack/files/liberty/keystone/wmfkeystonehooks/wmfkeystonehooks.py b/modules/openstack/files/liberty/keystone/wmfkeystonehooks/wmfkeystonehooks.py index 0456a9e..85c7f22 100644 --- a/modules/openstack/files/liberty/keystone/wmfkeystonehooks/wmfkeystonehooks.py +++ b/modules/openstack/files/liberty/keystone/wmfkeystonehooks/wmfkeystonehooks.py @@ -12,6 +12,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +import ldapgroups from keystoneclient.auth.identity import generic from keystoneclient import session as keystone_session @@ -58,6 +59,15 @@ cfg.StrOpt('admin_role_name', default='projectadmin', help='Name of project-local admin role'), + cfg.StrOpt('ldap_group_base_dn', + default='ou=groups,dc=wikimedia,dc=org', + help='ldap dn for posix groups'), + cfg.StrOpt('ldap_user_base_dn', + default='ou=people,dc=wikimedia,dc=org', + help='ldap dn for user accounts'), + cfg.IntOpt('minimum_gid_number', + default=40000, + help='Starting gid number for posix groups'), cfg.MultiStrOpt('eventtype_whitelist', default=['identity.project.deleted', 'identity.project.created'], help='Event types to always handle.'), @@ -80,18 +90,44 @@ def __init__(self, conf, topics, transport, version=1.0): pass - def _on_project_delete(self, project_id): - LOG.warning("Beginning wmf hooks for project deletion: %s" % project_id) - - def _on_project_create(self, project_id): - - LOG.warning("Beginning wmf hooks for project creation: %s" % project_id) - + def _get_role_dict(self): rolelist = self.role_api.list_roles() roledict = {} # Make a dict to relate role names to ids for role in rolelist: roledict[role['name']] = role['id'] + + return roledict + + def _get_current_assignments(self, project_id): + reverseroledict = dict((v, k) for k, v in self._get_role_dict().iteritems()) + + rawassignments = self.assignment_api.list_role_assignments(project_id=project_id) + assignments = {} + for assignment in rawassignments: + rolename = reverseroledict[assignment["role_id"]] + if rolename not in assignments: + assignments[rolename] = set() + assignments[rolename].add(assignment["user_id"]) + return assignments + + # There are a bunch of different events which might update project membership, + # and the generic 'identity.projectupdated' comes in the wrong order. So + # we're probably going to wind up getting called several times in quick succession, + # possible in overlapping invocations. Watch out for race conditions! + def _on_member_update(self, project_id): + assignments = self._get_current_assignments(project_id) + ldapgroups.sync_ldap_project_group(project_id, assignments) + + def _on_project_delete(self, project_id): + ldapgroups.delete_ldap_project_group(project_id) + + def _on_project_create(self, project_id): + + LOG.warning("Beginning wmf hooks for project creation: %s" % project_id) + + roledict = self._get_role_dict() + if CONF.wmfhooks.observer_role_name not in roledict.keys(): LOG.error("Failed to find id for role %s" % CONF.wmfhooks.observer_role_name) raise exception.NotImplemented() @@ -121,7 +157,7 @@ project_domain_name='Default', project_name=project_id) session = keystone_session.Session(auth=auth) - client = nova_client.Client('2', session=session) + client = nova_client.Client('2', session=session, connect_retries=5) allgroups = client.security_groups.list() defaultgroup = filter(lambda group: group.name == 'default', allgroups) if defaultgroup: @@ -182,6 +218,9 @@ else: LOG.warning("Failed to find default security group in new project.") + assignments = self._get_current_assignments(project_id) + ldapgroups.sync_ldap_project_group(project_id, assignments) + def notify(self, context, message, priority, retry=False): event_type = message.get('event_type') @@ -191,6 +230,10 @@ if event_type == 'identity.project.created': self._on_project_create(message['payload']['resource_info']) + if (event_type == 'identity.role_assignment.deleted' or + event_type == 'identity.role_assignment.created'): + self._on_member_update(message['payload']['project']) + # Eventually this will be used to update project resource pages: if event_type in CONF.wmfhooks.eventtype_blacklist: return -- To view, visit https://gerrit.wikimedia.org/r/338918 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Ibde67f8dd6c5a4d71a781730e9283b164a3544e4 Gerrit-PatchSet: 7 Gerrit-Project: operations/puppet Gerrit-Branch: production Gerrit-Owner: Andrew Bogott <abog...@wikimedia.org> Gerrit-Reviewer: Alex Monk <kren...@gmail.com> Gerrit-Reviewer: Andrew Bogott <abog...@wikimedia.org> Gerrit-Reviewer: Giuseppe Lavagetto <glavage...@wikimedia.org> Gerrit-Reviewer: Volans <rcocci...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits