From 5e098c23a7615ee1d7522abfa215d4e2f3331fe8 Mon Sep 17 00:00:00 2001 From: tianj7 Date: Wed, 20 Dec 2023 09:48:17 -0600 Subject: [PATCH] Add functionality to Aws Cognito to sync groups from ADFS --- fence/blueprints/login/cognito.py | 32 ++++++++++++++ fence/config-default.yaml | 2 + fence/resources/cognito/groups.py | 37 +++++++++++++++++ fence/resources/openid/cognito_oauth2.py | 19 +++++++++ fence/sync/sync_users.py | 53 ++++++++++++++++++++++++ 5 files changed, 143 insertions(+) create mode 100644 fence/resources/cognito/groups.py diff --git a/fence/blueprints/login/cognito.py b/fence/blueprints/login/cognito.py index 84a435dff..9f63a2fcb 100644 --- a/fence/blueprints/login/cognito.py +++ b/fence/blueprints/login/cognito.py @@ -2,6 +2,14 @@ from fence.blueprints.login.base import DefaultOAuth2Login, DefaultOAuth2Callback from fence.models import IdentityProvider +from cdislogging import get_logger + +from fence.config import config +from fence.blueprints.login.base import DefaultOAuth2Login, DefaultOAuth2Callback +import fence.resources.cognito.groups +from flask import current_app + +logger = get_logger(__name__) class CognitoLogin(DefaultOAuth2Login): @@ -16,3 +24,27 @@ def __init__(self): super(CognitoCallback, self).__init__( idp_name=IdentityProvider.cognito, client=flask.current_app.cognito_client ) + + def post_login(self, user=None, token_result=None, id_from_idp=None): + userinfo = flask.g.userinfo + + email = userinfo.get("email") + + assign_groups_as_policies = config["cognito"]["assign_groups_as_policies"] + assign_groups_claim_name = config["cognito"]["assign_groups_claim_name"] + + if assign_groups_as_policies: + try: + groups = flask.current_app.cognito_client.get_group_claims( + userinfo, assign_groups_claim_name + ) + except Exception as e: + err_msg = "Could not retrieve groups" + logger.error("{}: {}".format(e, err_msg)) + raise + + fence.resources.cognito.groups.sync_gen3_users_authz_from_adfs_groups( + email, groups, db_session=current_app.scoped_session() + ) + + super(CognitoCallback, self).post_login(id_from_idp=id_from_idp) diff --git a/fence/config-default.yaml b/fence/config-default.yaml index 279c048fb..4797cc999 100755 --- a/fence/config-default.yaml +++ b/fence/config-default.yaml @@ -238,6 +238,8 @@ OPENID_CONNECT: # and that IdP is a SAML IdP with no 'email_verified' outgoing claim, but it is safe # to assume all emails from this SAML IdP are in fact verified, we may set this to True assume_emails_verified: False + assign_groups_as_policies: True + assign_groups_claim_name: '' # CILogon subscribers can create and manage OIDC clients using COmanage Registry. # Free tier users may request OIDC clients at https://cilogon.org/oauth2/register cilogon: diff --git a/fence/resources/cognito/groups.py b/fence/resources/cognito/groups.py new file mode 100644 index 000000000..e4e1e8703 --- /dev/null +++ b/fence/resources/cognito/groups.py @@ -0,0 +1,37 @@ +import fence.scripting.fence_create + + +def sync_gen3_users_authz_from_adfs_groups(current_session, email, groups): + """ + Sync the authorization of users in the Gen3 database with the groups + they are in on the ADFS server. + Args: + groups (list): list of groups to sync + db_session (flask_sqlalchemy_session.SQLAlchemySession): db session to use + Return: + dict: dictionary of users that were synced and the groups they were + synced with + """ + # for each group, assign current user the following resources: + # /cohort-middleware/{group} + # with both role_ids: 'cohort_middleware_admin' and 'cohort_middleware_outputs_admin_reader' + db_session = db_session or current_session + _sync_adfs_groups( + email, + groups, + db_session=db_session, + ) + + +def _sync_adfs_groups(gen3_user, groups, current_session, db_session=None): + db_session = db_session or current_session + + default_args = fence.scripting.fence_create.get_default_init_syncer_inputs( + authz_provider="Cognito" + ) + syncer = fence.scripting.fence_create.init_syncer(**default_args) + + groups = syncer.sync_single_user_groups( + gen3_user, + groups, + ) diff --git a/fence/resources/openid/cognito_oauth2.py b/fence/resources/openid/cognito_oauth2.py index 73038c87f..72778d5bd 100644 --- a/fence/resources/openid/cognito_oauth2.py +++ b/fence/resources/openid/cognito_oauth2.py @@ -37,6 +37,25 @@ def get_auth_url(self): return uri + def get_group_claims(self, userinfo, claims): + """ + Return group claims from userinfo response + Args: + userinfo (dict): userinfo response + Return: + str: list of groups + """ + result = None + + attributes = userinfo.get("Attributes") + if attributes and len(attributes) > 0: + for a in attributes: + if a["Name"] == "custom:groups": + result = a["Value"] + break + + return result + def get_auth_info(self, code): """ Exchange code for tokens, get email from id token claims. diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 2803c4523..96fff3313 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -2520,3 +2520,56 @@ def sync_single_user_visas(self, user, ga4gh_visas, sess=None, expires=None): self.logger.error("No arborist client set; skipping arborist sync") return parsed_visas + + def sync_single_user_groups(self, user, groups, sess=None): + """ + Sync a single user's groups during login + Args: + user (userdatamodel.user.User): Fence user whose group + authz info is being synced + groups (list): a list of groups that the user is a member of + Return: + list of successfully assigned groups + """ + try: + user_yaml = UserYAML.from_file( + self.sync_from_local_yaml_file, encrypted=False, logger=self.logger + ) + except (EnvironmentError, AssertionError) as e: + self.logger.error(str(e)) + self.logger.error("aborting early") + raise + + user_projects = dict() + projects = {} + + for group in groups: + project = {} + privileges = {"read-storage", "read"} + project[group] = privileges + projects = {**projects, **project} + + user_projects[user.username] = projects + user_projects = self.parse_projects(user_projects) + + # update arborist db (user access) + if self.arborist_client: + self.logger.info("Synchronizing arborist with authorization info...") + success = self._update_authz_in_arborist( + sess, + user_projects, + user_yaml=user_yaml, + single_user_sync=True, + ) + if success: + self.logger.info( + "Finished synchronizing authorization info to arborist" + ) + else: + self.logger.error( + "Could not synchronize authorization info successfully to arborist" + ) + else: + self.logger.error("No arborist client set; skipping arborist sync") + + return