From e7b59058b20db56ab7f648c572b3d55cd886cfd1 Mon Sep 17 00:00:00 2001 From: Dean Rasheed <dar17@cam.ac.uk> Date: Thu, 13 Aug 2020 17:49:53 +0100 Subject: [PATCH] Synchronise Lookup institutions. This causes Lookup institutions and their members to be synchronised with groups in Google, in a similar way to Lookup groups. The unique identifier of the group in Google is it's email address: email = {instID.lower()}@{insts_domain} where {insts_domain} is a configurable secondary domain (noting that the local part of the email address is required to be lowercase, whereas all instIDs from Lookup are uppercase). --- configuration-example.yaml | 18 +++ gsuitesync/gapidomain.py | 4 + gsuitesync/ldap.py | 63 +++++++++ gsuitesync/sync.py | 281 ++++++++++++++++++++++++------------- 4 files changed, 267 insertions(+), 99 deletions(-) diff --git a/configuration-example.yaml b/configuration-example.yaml index 31ce46c..0481973 100644 --- a/configuration-example.yaml +++ b/configuration-example.yaml @@ -150,6 +150,9 @@ ldap: # LDAP search base for groups. Group filters are always relative to this. group_search_base: 'ou=groups,o=example-corps,dc=example,dc=com' + # LDAP search base for institutions. Institution filters are always relative to this. + inst_search_base: 'ou=insts,o=example-corps,dc=example,dc=com' + # Filter to use to determine the "eligible" list of users. If a non-admin user # is found on Google who isn't in this list, their account will be suspended. eligible_user_filter: '(uid=*)' @@ -158,6 +161,10 @@ ldap: # found on Google that isn't in this list, it will be deleted. eligible_group_filter: '(groupID=*)' + # Filter to use to determine the "eligible" list of institutions. If an + # institution is found on Google that isn't in this list, it will be deleted. + eligible_inst_filter: '(instID=*)' + # Filter to use to determine the "managed" list of users. If a user appears in # this list who isn't in Google their account is created. If the user metadata # for a user in this list changes, the change is propagated to Google. If @@ -171,6 +178,13 @@ ldap: # Default: null. managed_group_filter: null + # Filter to use to determine the "managed" list of institutions. If an + # institution appears in this list that isn't in Google it is created. If the + # institution metadata or list of members for an institution in this list + # changes, the change is propagated to Google. If null, the value of + # "eligible_inst_filter" is used. Default: null. + managed_inst_filter: null + # Details about the Google Domain we're managing. google_domain: # Name of the domain. @@ -185,3 +199,7 @@ google_domain: # Secondary domain or domain alias for groups. If null, the value of "name" # is used. Default: null groups_domain: null + + # Secondary domain or domain alias for institutions. If null, the value of + # "name" is used. Default: null + insts_domain: null diff --git a/gsuitesync/gapidomain.py b/gsuitesync/gapidomain.py index 9e7e1eb..21d0923 100644 --- a/gsuitesync/gapidomain.py +++ b/gsuitesync/gapidomain.py @@ -25,3 +25,7 @@ class Configuration(ConfigurationDataclassMixin): # Secondary domain or domain alias for groups. If None, the value of "name" is used. # Default: None groups_domain: typing.Union[str, None] = None + + # Secondary domain or domain alias for institutions. If None, the value of "name" is used. + # Default: None + insts_domain: typing.Union[str, None] = None diff --git a/gsuitesync/ldap.py b/gsuitesync/ldap.py index fa39881..62d8447 100644 --- a/gsuitesync/ldap.py +++ b/gsuitesync/ldap.py @@ -28,14 +28,20 @@ class Configuration(ConfigurationDataclassMixin): group_search_base: str + inst_search_base: str + eligible_user_filter: str eligible_group_filter: str + eligible_inst_filter: str + managed_user_filter: typing.Union[str, None] = None managed_group_filter: typing.Union[str, None] = None + managed_inst_filter: typing.Union[str, None] = None + def get_eligible_uids(self): """ Return a set containing all uids who are eligible to have a Google account. @@ -62,6 +68,19 @@ class Configuration(ConfigurationDataclassMixin): ) } + def get_eligible_instIDs(self): + """ + Return a set containing all instIDs that are eligible for Google. + + """ + return { + e['attributes']['instID'][0] + for e in self._search( + search_base=self.inst_search_base, search_filter=self.eligible_inst_filter, + attributes=['instID'] + ) + } + def get_managed_user_entries(self): """ Return a list containing all managed user entries as UserEntry instances. @@ -104,6 +123,50 @@ class Configuration(ConfigurationDataclassMixin): ) ] + def get_managed_inst_entries(self): + """ + Return a list containing all managed institution entries as GroupEntry instances. + + Note that we return GroupEntry instances here since Lookup institutions become groups in + Google, and this simplifies the sync code by allowing us to handle institutions in the same + way as groups. The GroupEntry's groupID and groupName fields will be the institution's + instID and ou (name) respectively. Since Lookup institutions don't have descriptions, we + set the description field to the institution's name as well (in Google, the description + allows longer strings, and so will not truncate the name). + + """ + # This requires 2 LDAP queries. First find the managed institutions. + search_filter = ( + self.managed_inst_filter + if self.managed_inst_filter is not None + else self.eligible_inst_filter + ) + managed_insts = [ + GroupEntry( + groupID=_extract(e, 'instID'), groupName=_extract(e, 'ou'), + description=_extract(e, 'ou'), uids=set(), + ) + for e in self._search( + search_base=self.inst_search_base, search_filter=search_filter, + attributes=['instID', 'ou'] + ) + ] + managed_insts_by_instID = {g.groupID: g for g in managed_insts} + + # Then get each eligible user's list of institutions and use that data to populate each + # institution's uid list. + eligible_users = self._search( + search_base=self.user_search_base, search_filter=self.eligible_user_filter, + attributes=['uid', 'instID'] + ) + for e in eligible_users: + uid = e['attributes']['uid'][0] + for instID in e['attributes']['instID']: + if instID in managed_insts_by_instID: + managed_insts_by_instID[instID].uids.add(uid) + + return managed_insts + def _search(self, *, search_base, search_filter, attributes): ldap_server = ldap3.Server(self.host) with ldap3.Connection(ldap_server, auto_bind=True) as conn: diff --git a/gsuitesync/sync.py b/gsuitesync/sync.py index 66c5f2e..a7c5f61 100644 --- a/gsuitesync/sync.py +++ b/gsuitesync/sync.py @@ -110,37 +110,60 @@ def sync(configuration, *, read_only=True): .with_subject(gapi_domain_config.admin_user) ) - # Functions to translate the unique identifiers of users and groups in Lookup (uids and - # groupIDs) to and from the unique identifiers used in Google (email addresses). - # - # For users: {uid} <-> {uid}@{domain} - # For groups: {groupID} <-> {groupID}@{groups_domain} - # - # Additionally, valid uids (CRSids) match the regex [a-z][a-z0-9]{3,7}, and valid groupIDs - # match the regex [0-9]{6,8}. - user_email_regex = re.compile('^[a-z][a-z0-9]{3,7}@.*$') - group_email_regex = re.compile('^[0-9]{6,8}@.*$') - + # Secondary domain for Google groups that come from Lookup groups groups_domain = ( gapi_domain_config.groups_domain if gapi_domain_config.groups_domain is not None else gapi_domain_config.name ) + # Secondary domain for Google groups that come from Lookup institutions + insts_domain = ( + gapi_domain_config.insts_domain + if gapi_domain_config.insts_domain is not None + else gapi_domain_config.name + ) + + # Functions to translate the unique identifiers of users, groups and institutions in Lookup + # (uids, groupIDs and instIDs) to and from the unique identifiers used in Google (email + # addresses). + # + # For users: {uid} <-> {uid}@{domain} + # For groups: {groupID} <-> {groupID}@{groups_domain} + # For insts: {instID} <-> {instID.lower()}@{insts_domain} (local part must be lowercase) + # + # Additionally, valid uids (CRSids) match the regex [a-z][a-z0-9]{3,7}, valid groupIDs match + # the regex [0-9]{6,8} and valid instIDs match the regex [A-Z][A-Z0-9]+. + # + # Since Lookup institutions become groups in Google, we use common code to sync all Google + # groups, regardless of whether they were groups or institutions in Lookup. In all the code + # that follows, we use "gid" to refer to the unique identifier of the group or institution in + # Lookup (i.e., gid may be either a Lookup groupID or instID). + user_email_regex = re.compile('^[a-z][a-z0-9]{3,7}@.*$') + groupID_regex = re.compile('^[0-9]{6,8}$') + instID_regex = re.compile('^[A-Z][A-Z0-9]+$') + def uid_to_email(uid): return f'{uid}@{gapi_domain_config.name}' def email_to_uid(email): return email.split('@')[0] if user_email_regex.match(email) else None - def groupID_to_email(groupID): - return f'{groupID}@{groups_domain}' + def gid_to_email(gid): + return ( + f'{gid}@{groups_domain}' if groupID_regex.match(gid) else + f'{gid.lower()}@{insts_domain}' if instID_regex.match(gid) else None + ) - def email_to_groupID(email): - return email.split('@')[0] if group_email_regex.match(email) else None + def email_to_gid(email): + gid = email.split('@')[0] + return ( + gid if groupID_regex.match(gid) else + gid.upper() if instID_regex.match(gid.upper()) else None + ) # -------------------------------------------------------------------------------------------- - # Load current user and group data from Lookup. + # Load current user, group and institution data from Lookup. # -------------------------------------------------------------------------------------------- # Get a set containing all CRSids. These are all the people who are eligible to be in our @@ -157,6 +180,18 @@ def sync(configuration, *, read_only=True): eligible_groupIDs = ldap_config.get_eligible_groupIDs() LOG.info('Total LDAP group entries: %s', len(eligible_groupIDs)) + # Get a set containing all instIDs. These are all the institutions that are eligible to be in + # our GSuite instance. If an institution is in GSuite and is *not* present in this list then + # the corresponding group is deleted. + LOG.info('Reading eligible institution entries from LDAP') + eligible_instIDs = ldap_config.get_eligible_instIDs() + LOG.info('Total LDAP institution entries: %s', len(eligible_instIDs)) + + # Add these sets together to form the set of all gids (the IDs of all eligible groups and + # institutions). + eligible_gids = eligible_groupIDs | eligible_instIDs + LOG.info('Total combined LDAP group and institution entries: %s', len(eligible_gids)) + # Get a list of managed users. These are all the people who match the "managed_user_filter" in # the LDAP settings. LOG.info('Reading managed user entries from LDAP') @@ -186,14 +221,41 @@ def sync(configuration, *, read_only=True): LOG.info('Total managed group entries: %s', len(managed_group_groupIDs)) LOG.info('Total managed group members: %s', sum([len(g.uids) for g in managed_group_entries])) + # Get a list of managed institutions. These are all the institutions that match the + # "managed_inst_filter" in the LDAP settings. + LOG.info('Reading managed institution entries from LDAP') + managed_inst_entries = ldap_config.get_managed_inst_entries() + + # Form a mapping from instID to managed institution. + managed_inst_entries_by_instID = {i.groupID: i for i in managed_inst_entries} + + # Form a set of all *managed institution* instIDs + managed_inst_instIDs = set(managed_inst_entries_by_instID.keys()) + LOG.info('Total managed institution entries: %s', len(managed_inst_instIDs)) + LOG.info( + 'Total managed institution members: %s', sum([len(i.uids) for i in managed_inst_entries]) + ) + + # Add the collections of managed institutions to the collections of managed groups. + managed_group_entries += managed_inst_entries + managed_group_entries_by_gid = { + **managed_group_entries_by_groupID, **managed_inst_entries_by_instID + } + managed_group_gids = managed_group_groupIDs | eligible_instIDs + LOG.info( + 'Total combined managed group and institution entries: %s', len(managed_group_gids) + ) + LOG.info( + 'Total combined managed group and institution members: %s', + sum([len(g.uids) for g in managed_group_entries]) + ) + # Sanity check: the managed groups should be a subset of the eligible ones. - if len(managed_group_groupIDs - eligible_groupIDs) != 0: - raise RuntimeError( - 'Sanity check failed: some managed groupIDs were not in the eligible set' - ) + if len(managed_group_gids - eligible_gids) != 0: + raise RuntimeError('Sanity check failed: some managed gids were not in the eligible set') # -------------------------------------------------------------------------------------------- - # Load current user and group data from Google. + # Load current user, group and institution data from Google. # -------------------------------------------------------------------------------------------- # Build the directory service using Google API discovery. @@ -214,14 +276,28 @@ def sync(configuration, *, read_only=True): retries=sync_config.http_retries, retry_delay=sync_config.http_retry_delay, ) - # Retrieve information on all groups + # Function to fetch Google group information from the specified domain + def fetch_groups(domain): + fields = ['id', 'email', 'name', 'description'] + return gapiutil.list_all( + directory_service.groups().list, items_key='groups', domain=domain, + fields='nextPageToken,groups(' + ','.join(fields) + ')', + retries=sync_config.http_retries, retry_delay=sync_config.http_retry_delay, + ) + + # Retrieve information on all Google groups that come from Lookup groups LOG.info('Getting information on Google domain groups') - fields = ['id', 'email', 'name', 'description'] - all_google_groups = gapiutil.list_all( - directory_service.groups().list, items_key='groups', domain=groups_domain, - fields='nextPageToken,groups(' + ','.join(fields) + ')', - retries=sync_config.http_retries, retry_delay=sync_config.http_retry_delay, - ) + all_google_groups = [ + g for g in fetch_groups(groups_domain) + if groupID_regex.match(g['email'].split('@')[0]) + ] + + # Append information on all Google groups that come from Lookup institutions + LOG.info('Getting information on Google domain institutions') + all_google_groups.extend([ + g for g in fetch_groups(insts_domain) + if instID_regex.match(g['email'].split('@')[0].upper()) + ]) # Strip any "to be ignored" users out of the results. if sync_config.ignore_google_org_unit_path_regex is not None: @@ -250,30 +326,31 @@ def sync(configuration, *, read_only=True): all_google_users = [u for u in all_google_users if email_to_uid(u['primaryEmail'])] # Strip out any groups whose email addresses don't match the pattern for groups created from - # Lookup groupIDs, and which therefore should not be managed (deleted) by this script. - all_google_groups = [g for g in all_google_groups if email_to_groupID(g['email'])] + # Lookup groupIDs or instIDs, and which therefore should not be managed (deleted) by this + # script. + all_google_groups = [g for g in all_google_groups if email_to_gid(g['email'])] # Sanity check. There should be no admins in the returned results. if any(u.get('isAdmin', False) for u in all_google_users): raise RuntimeError('Sanity check failed: admin users in user list') - # Form mappings from uid/groupID to Google user/group. + # Form mappings from uid/gid to Google user/group. all_google_users_by_uid = {email_to_uid(u['primaryEmail']): u for u in all_google_users} - all_google_groups_by_groupID = {email_to_groupID(g['email']): g for g in all_google_groups} + all_google_groups_by_gid = {email_to_gid(g['email']): g for g in all_google_groups} - # Form sets of all Google-side uids and groupIDs. The all_google_uids set is all users - # including the suspended ones and the suspended_google_uids set is only the suspended users. - # Non suspended users are therefore all_google_uids - suspended_google_uids. Groups do not have - # any concept of being suspended. + # Form sets of all Google-side uids and gids. The all_google_uids set is all users including + # the suspended ones and the suspended_google_uids set is only the suspended users. Non + # suspended users are therefore all_google_uids - suspended_google_uids. The all_google_gids + # set includes both groupIDs and instIDs. Groups in Google do not have any concept of being + # suspended. all_google_uids = set(all_google_users_by_uid.keys()) - all_google_groupIDs = set(all_google_groups_by_groupID.keys()) + all_google_gids = set(all_google_groups_by_gid.keys()) suspended_google_uids = {uid for uid, u in all_google_users_by_uid.items() if u['suspended']} - # Sanity check. We should not have lost anything. (I.e. the uids and groupIDs should be - # unique.) + # Sanity check. We should not have lost anything. (I.e. the uids and gids should be unique.) if len(all_google_uids) != len(all_google_users): raise RuntimeError('Sanity check failed: user list changed length') - if len(all_google_groupIDs) != len(all_google_groups): + if len(all_google_gids) != len(all_google_groups): raise RuntimeError('Sanity check failed: group list changed length') # Retrieve all Google group settings. @@ -285,17 +362,17 @@ def sync(configuration, *, read_only=True): retries=sync_config.http_retries, retry_delay=sync_config.http_retry_delay, ) - # Form a mapping from groupID to Google group settings. - all_google_group_settings_by_groupID = { - email_to_groupID(g['email']): g for g in all_google_group_settings + # Form a mapping from gid to Google group settings. + all_google_group_settings_by_gid = { + email_to_gid(g['email']): g for g in all_google_group_settings } # Santiy check. We should have settings for each managed group. - if len(all_google_group_settings_by_groupID) != len(all_google_groups): + if len(all_google_group_settings_by_gid) != len(all_google_groups): raise RuntimeError('Sanity check failed: group settings list does not match group list') # Retrieve all Google group memberships. This is a mapping from internal Google group ids to - # lists of member resources. + # lists of member resources, corresponding to both Lookup groups and institutions. fields = ['id', 'email'] all_google_members = gapiutil.list_all_in_list( directory_service, directory_service.members().list, @@ -311,7 +388,7 @@ def sync(configuration, *, read_only=True): # Log some stats. LOG.info('Total Google users: %s', len(all_google_uids)) - LOG.info('Total Google groups: %s', len(all_google_groupIDs)) + LOG.info('Total Google groups: %s', len(all_google_gids)) LOG.info( 'Suspended Google users: %s', sum(1 if u['suspended'] else 0 for u in all_google_users)) LOG.info( @@ -377,12 +454,18 @@ def sync(configuration, *, read_only=True): # if it needs updating/creating. If so, record a patch/insert for the group. google_group_updates = {} google_group_creations = {} - for groupID, managed_group_entry in managed_group_entries_by_groupID.items(): + for gid, managed_group_entry in managed_group_entries_by_gid.items(): # Form expected group resource fields. The 2 Google APIs we use here to update groups in # Google each have different maximum lengths for group names and descriptions, and # empirically the APIs don't function properly if either limit is exceeded, so we use the # minimum of the 2 documented maximum field lengths (73 characters for names and 300 # characters for descriptions). + # + # Note that the source of each of these groups may be either a Lookup group or a Lookup + # institution, which are handled the same here. Technically Lookup institutions do not have + # descriptions, but the code in ldap.py sets the description from the name for Lookup + # institutions, which is useful since some institution names do not fit in the Google name + # field. expected_google_group = { 'name': _trim_text( managed_group_entry.groupName, maxlen=73, suffix=sync_config.group_name_suffix @@ -391,7 +474,7 @@ def sync(configuration, *, read_only=True): } # Find existing Google group (if any). - existing_google_group = all_google_groups_by_groupID.get(groupID) + existing_google_group = all_google_groups_by_gid.get(gid) if existing_google_group is not None: # See if we need to change the existing group @@ -405,11 +488,11 @@ def sync(configuration, *, read_only=True): # Only record non-empty patches. if len(patch) > 0: - google_group_updates[groupID] = patch + google_group_updates[gid] = patch else: # No existing Google group, so create one. - google_group_creations[groupID] = { - 'email': groupID_to_email(groupID), + google_group_creations[gid] = { + 'email': gid_to_email(gid), **expected_google_group } @@ -417,17 +500,17 @@ def sync(configuration, *, read_only=True): uids_to_update = set(google_user_updates.keys()) LOG.info('Number of existing users to update: %s', len(uids_to_update)) - # Form a set of all the groupIDs which need patching. - groupIDs_to_update = set(google_group_updates.keys()) - LOG.info('Number of existing groups to update: %s', len(groupIDs_to_update)) + # Form a set of all the gids which need patching. + gids_to_update = set(google_group_updates.keys()) + LOG.info('Number of existing groups to update: %s', len(gids_to_update)) # Form a set of all the uids which need adding. uids_to_add = set(google_user_creations.keys()) LOG.info('Number of users to add: %s', len(uids_to_add)) - # Form a set of all the groupIDs which need adding. - groupIDs_to_add = set(google_group_creations.keys()) - LOG.info('Number of groups to add: %s', len(groupIDs_to_add)) + # Form a set of all the gids which need adding. + gids_to_add = set(google_group_creations.keys()) + LOG.info('Number of groups to add: %s', len(gids_to_add)) # Form a set of all uids which need reactivating. We reactive users who are in the managed user # list *and* the suspended user list. @@ -439,17 +522,17 @@ def sync(configuration, *, read_only=True): uids_to_suspend = (all_google_uids - suspended_google_uids) - eligible_uids LOG.info('Number of users to suspend: %s', len(uids_to_suspend)) - # Form a set of all groupIDs which need deleting. - groupIDs_to_delete = all_google_groupIDs - eligible_groupIDs - LOG.info('Number of groups to delete: %s', len(groupIDs_to_delete)) + # Form a set of all gids which need deleting. + gids_to_delete = all_google_gids - eligible_gids + LOG.info('Number of groups to delete: %s', len(gids_to_delete)) # For each managed group, determine which members to insert or delete. These are lists of - # (groupID, uid) tuples. + # (gid, uid) tuples. members_to_insert = [] members_to_delete = [] - for groupID, managed_group_entry in managed_group_entries_by_groupID.items(): + for gid, managed_group_entry in managed_group_entries_by_gid.items(): # Find the existing Google group members. - existing_google_group = all_google_groups_by_groupID.get(groupID) + existing_google_group = all_google_groups_by_gid.get(gid) if existing_google_group: existing_members = all_google_members[existing_google_group['id']] existing_member_uids = set([email_to_uid(m['email']) for m in existing_members]) @@ -461,7 +544,7 @@ def sync(configuration, *, read_only=True): insert_uids = ( (managed_group_entry.uids - existing_member_uids).intersection(managed_user_uids) ) - members_to_insert.extend([(groupID, uid) for uid in insert_uids]) + members_to_insert.extend([(gid, uid) for uid in insert_uids]) # Members to delete. This is restricted to the eligible user set, so that we don't bother # to delete a member resource when the user is suspended (and so we won't need to re-add @@ -469,7 +552,7 @@ def sync(configuration, *, read_only=True): delete_uids = ( (existing_member_uids - managed_group_entry.uids).intersection(eligible_uids) ) - members_to_delete.extend([(groupID, uid) for uid in delete_uids]) + members_to_delete.extend([(gid, uid) for uid in delete_uids]) LOG.info('Number of group members to insert: %s', len(members_to_insert)) LOG.info('Number of group members to delete: %s', len(members_to_delete)) @@ -487,9 +570,9 @@ def sync(configuration, *, read_only=True): LOG.info('Configuration will modify %.2f%% of users', user_change_percentage) group_change_percentage = 100. * ( - len(groupIDs_to_add | groupIDs_to_update | groupIDs_to_delete) + len(gids_to_add | gids_to_update | gids_to_delete) / - max(1, len(all_google_groupIDs)) + max(1, len(all_google_gids)) ) LOG.info('Configuration will modify %.2f%% of groups', group_change_percentage) @@ -533,22 +616,22 @@ def sync(configuration, *, read_only=True): uids_to_add = capped_uids_to_add LOG.info('Capped number of new users to %s', len(uids_to_add)) if (limits_config.max_new_groups is not None and - len(groupIDs_to_add) > limits_config.max_new_groups): + len(gids_to_add) > limits_config.max_new_groups): # Ensure that we do not attempt to insert a group member for any of the groups not added # as a result of this cap, since these groups won't exist in Google - capped_groupIDs_to_add = _limit(groupIDs_to_add, limits_config.max_new_groups) - groupIDs_not_added = groupIDs_to_add - capped_groupIDs_to_add - members_to_insert = [(g, u) for g, u in members_to_insert if g not in groupIDs_not_added] - groupIDs_to_add = capped_groupIDs_to_add - LOG.info('Capped number of new groups to %s', len(groupIDs_to_add)) + capped_gids_to_add = _limit(gids_to_add, limits_config.max_new_groups) + gids_not_added = gids_to_add - capped_gids_to_add + members_to_insert = [(g, u) for g, u in members_to_insert if g not in gids_not_added] + gids_to_add = capped_gids_to_add + LOG.info('Capped number of new groups to %s', len(gids_to_add)) if (limits_config.max_suspended_users is not None and len(uids_to_suspend) > limits_config.max_suspended_users): uids_to_suspend = _limit(uids_to_suspend, limits_config.max_suspended_users) LOG.info('Capped number of users to suspend to %s', len(uids_to_suspend)) if (limits_config.max_deleted_groups is not None and - len(groupIDs_to_delete) > limits_config.max_deleted_groups): - groupIDs_to_delete = _limit(groupIDs_to_delete, limits_config.max_deleted_groups) - LOG.info('Capped number of groups to delete to %s', len(groupIDs_to_delete)) + len(gids_to_delete) > limits_config.max_deleted_groups): + gids_to_delete = _limit(gids_to_delete, limits_config.max_deleted_groups) + LOG.info('Capped number of groups to delete to %s', len(gids_to_delete)) if (limits_config.max_reactivated_users is not None and len(uids_to_reactivate) > limits_config.max_reactivated_users): uids_to_reactivate = _limit(uids_to_reactivate, limits_config.max_reactivated_users) @@ -558,9 +641,9 @@ def sync(configuration, *, read_only=True): uids_to_update = _limit(uids_to_update, limits_config.max_updated_users) LOG.info('Capped number of users to update to %s', len(uids_to_update)) if (limits_config.max_updated_groups is not None and - len(groupIDs_to_update) > limits_config.max_updated_groups): - groupIDs_to_update = _limit(groupIDs_to_update, limits_config.max_updated_groups) - LOG.info('Capped number of groups to update to %s', len(groupIDs_to_update)) + len(gids_to_update) > limits_config.max_updated_groups): + gids_to_update = _limit(gids_to_update, limits_config.max_updated_groups) + LOG.info('Capped number of groups to update to %s', len(gids_to_update)) if (limits_config.max_inserted_members is not None and len(members_to_insert) > limits_config.max_inserted_members): members_to_insert = members_to_insert[0:limits_config.max_inserted_members] @@ -612,55 +695,55 @@ def sync(configuration, *, read_only=True): # service to perform the actions required to update groups def group_api_requests(): # Update existing groups - group_updates = {groupID: google_group_updates[groupID] for groupID in groupIDs_to_update} - for groupID, update in group_updates.items(): - google_id = all_google_groups_by_groupID[groupID]['id'] - LOG.info('Update group "%s": "%r"', groupID, update) + group_updates = {gid: google_group_updates[gid] for gid in gids_to_update} + for gid, update in group_updates.items(): + google_id = all_google_groups_by_gid[gid]['id'] + LOG.info('Update group "%s": "%r"', gid, update) yield directory_service.groups().patch(groupKey=google_id, body=update) # Delete cancelled groups - for groupID in groupIDs_to_delete: - google_id = all_google_groups_by_groupID[groupID]['id'] - LOG.info('Deleting group: "%s"', groupID) + for gid in gids_to_delete: + google_id = all_google_groups_by_gid[gid]['id'] + LOG.info('Deleting group: "%s"', gid) yield directory_service.groups().delete(groupKey=google_id) # Create new groups - for groupID in groupIDs_to_add: - new_group = google_group_creations[groupID] - LOG.info('Adding group "%s": %s', groupID, new_group) + for gid in gids_to_add: + new_group = google_group_creations[gid] + LOG.info('Adding group "%s": %s', gid, new_group) yield directory_service.groups().insert(body=new_group) # A generator which will generate patch() calls to the groupssettings service to set or # update the required group settings. def group_settings_api_requests(): # Apply all settings to new groups. - for groupID in groupIDs_to_add: - email = groupID_to_email(groupID) + for gid in gids_to_add: + email = gid_to_email(gid) settings = sync_config.group_settings - LOG.info('Updating settings for new group "%s": %s', groupID, settings) + LOG.info('Updating settings for new group "%s": %s', gid, settings) yield groupssettings_service.groups().patch(groupUniqueId=email, body=settings) # Apply any settings that differ to pre-existing groups. - for groupID, settings in all_google_group_settings_by_groupID.items(): + for gid, settings in all_google_group_settings_by_gid.items(): patch = {k: v for k, v in sync_config.group_settings.items() if settings.get(k) != v} if patch: - email = groupID_to_email(groupID) - LOG.info('Updating settings for existing group "%s": %s', groupID, patch) + email = gid_to_email(gid) + LOG.info('Updating settings for existing group "%s": %s', gid, patch) yield groupssettings_service.groups().patch(groupUniqueId=email, body=patch) # A generator which will generate insert() and delete() calls to the directory service to # perform the actions required to update group members def member_api_requests(): # Insert new members - for groupID, uid in members_to_insert: - group_key = groupID_to_email(groupID) + for gid, uid in members_to_insert: + group_key = gid_to_email(gid) user_key = uid_to_email(uid) LOG.info('Adding user "%s" to group "%s"', user_key, group_key) yield directory_service.members().insert(groupKey=group_key, body={'email': user_key}) # Delete removed members - for groupID, uid in members_to_delete: - group_key = groupID_to_email(groupID) + for gid, uid in members_to_delete: + group_key = gid_to_email(gid) user_key = uid_to_email(uid) LOG.info('Removing user "%s" from group "%s"', user_key, group_key) yield directory_service.members().delete(groupKey=group_key, memberKey=user_key) -- GitLab