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

Reply via email to