diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/adapters/keycloak.py b/SAS/TMSS/backend/src/tmss/tmssapp/adapters/keycloak.py index d36413a4d56bf098ae2dc754da2773a8bfa856cf..7ddf72eb7a128fb49a32265074126604be029db7 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/adapters/keycloak.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/adapters/keycloak.py @@ -44,7 +44,12 @@ def get_users_by_role_in_project(role, project): """ returns the list of users that have the specified role in the specified project """ - project_persons = get_project_persons() + # we fetch all and cache them instead of hitting Keycloak every time this gets called. + # But we don't care about all the legacy projects, so only request what's known to TMSS. + # This also works but is a little less efficient (for a relatively small number of projects): + # project_persons = get_project_persons(include_projects=(project,)) + projects_known_to_tmss = models.Project.objects.values_list('name', flat=True) + project_persons = get_project_persons(include_projects=tuple(projects_known_to_tmss)) if project in project_persons: return project_persons[project][role] else: @@ -52,10 +57,11 @@ def get_users_by_role_in_project(role, project): @cachetools.func.ttl_cache(ttl=600) -def get_project_persons(): +def get_project_persons(include_projects: tuple = None): """ returns a mapping of projects names to a dict that contains the users that have a particular role in that project. + Optionally filter for a list of projects to include in the response. """ project_persons_map = {} with KeycloakAdminAPISession() as ksession: @@ -64,47 +70,48 @@ def get_project_persons(): if group['name'] == 'Project': projects = group['subGroups'] for project in projects: - project_detail = ksession.get(url='%s/groups/%s/' % (KEYCLOAK_API_BASE_URL, project['id'])) - attributes = project_detail.get('attributes', {}) - - legacy_role_keys = {'pi': 'lofarProjectPI', - 'friend_of_project': 'lofarProjectFriend', - 'contact': 'lofarProjectContactauthor'} - - for project_role in models.ProjectRole.Choices: - # get role attribute from project: - role = project_role.value - users = attributes.get(role, []) - # fall back to legacy-style attribute: - if not users and role in legacy_role_keys: - users = attributes.get(legacy_role_keys[role], []) - - # convert user list (LDAP DNs) to something we can use in TMSS (email) - user_map = get_user_mapping() - mapped_users = [user_map[user] for user in users if user in user_map] # email list of referenced users - unmappable_users = [user for user in users if user not in user_map] # list of references for which no account was found - for unmappable_user in unmappable_users: - # Note: Usually Keycloak should return DN references to user accounts. For PI's, someone had the - # great idea to allow to specify a freeform string instead, to refer to people who may or may not - # have an account. Even if the person has a user account, there is no way to replicate the exact - # string 'representation' Keycloak returns, since the string may contain typos, or info that is not - # stored in the user accounts (like titles). - # The following unsafe hack tries to determine whether there is a user account that matches the - # name given in the string (ignore titles since they are not part of the user account): - # unmappable_user_fixed = re.sub('Dr\.', '', unmappable_user) - # unmappable_user_fixed = re.sub('Prof\.', '', unmappable_user_fixed) - # unmappable_user_fixed = re.sub('ir\.', '', unmappable_user_fixed) - # unmappable_user_fixed = re.sub('Ir\.', '', unmappable_user_fixed) - # unmappable_user_fixed = re.sub('apl\.', '', unmappable_user_fixed) - # unmappable_user_fixed = re.sub(' +', ' ', unmappable_user_fixed) - # - # if unmappable_user_fixed in user_map: - # mapped_users.append(user_map[unmappable_user_fixed]) - # else: - logger.warning("Could not match Keycloak user reference '%s' to a known user." % unmappable_user) - if not unmappable_user.startswith('cn='): - logger.warning("LOFAR allowed to reference a person by a freeform string instead of a user account. '%s' seems to be such a legacy reference. This needs to be fixed in the identity management." % unmappable_user) - project_persons_map.setdefault(project['name'], {})[role] = mapped_users + if include_projects is None or project['name'] in include_projects: + project_detail = ksession.get(url='%s/groups/%s/' % (KEYCLOAK_API_BASE_URL, project['id'])) + attributes = project_detail.get('attributes', {}) + + legacy_role_keys = {'pi': 'lofarProjectPI', + 'friend_of_project': 'lofarProjectFriend', + 'contact': 'lofarProjectContactauthor'} + + for project_role in models.ProjectRole.Choices: + # get role attribute from project: + role = project_role.value + users = attributes.get(role, []) + # fall back to legacy-style attribute: + if not users and role in legacy_role_keys: + users = attributes.get(legacy_role_keys[role], []) + + # convert user list (LDAP DNs) to something we can use in TMSS (email) + user_map = get_user_mapping() + mapped_users = [user_map[user] for user in users if user in user_map] # email list of referenced users + unmappable_users = [user for user in users if user not in user_map] # list of references for which no account was found + for unmappable_user in unmappable_users: + # Note: Usually Keycloak should return DN references to user accounts. For PI's, someone had the + # great idea to allow to specify a freeform string instead, to refer to people who may or may not + # have an account. Even if the person has a user account, there is no way to replicate the exact + # string 'representation' Keycloak returns, since the string may contain typos, or info that is not + # stored in the user accounts (like titles). + # The following unsafe hack tries to determine whether there is a user account that matches the + # name given in the string (ignore titles since they are not part of the user account): + # unmappable_user_fixed = re.sub('Dr\.', '', unmappable_user) + # unmappable_user_fixed = re.sub('Prof\.', '', unmappable_user_fixed) + # unmappable_user_fixed = re.sub('ir\.', '', unmappable_user_fixed) + # unmappable_user_fixed = re.sub('Ir\.', '', unmappable_user_fixed) + # unmappable_user_fixed = re.sub('apl\.', '', unmappable_user_fixed) + # unmappable_user_fixed = re.sub(' +', ' ', unmappable_user_fixed) + # + # if unmappable_user_fixed in user_map: + # mapped_users.append(user_map[unmappable_user_fixed]) + # else: + logger.warning("Could not match Keycloak user reference '%s' to a known user." % unmappable_user) + if not unmappable_user.startswith('cn='): + logger.warning("LOFAR allowed to reference a person by a freeform string instead of a user account. '%s' seems to be such a legacy reference. This needs to be fixed in the identity management." % unmappable_user) + project_persons_map.setdefault(project['name'], {})[role] = mapped_users return project_persons_map