diff --git a/IM/InfrastructureManager.py b/IM/InfrastructureManager.py index 39d6a3f0f..b6e311cee 100644 --- a/IM/InfrastructureManager.py +++ b/IM/InfrastructureManager.py @@ -1425,6 +1425,20 @@ def check_oidc_token(im_auth): if not issuer.endswith('/'): issuer += '/' im_auth['password'] = issuer + str(userinfo.get("sub")) + + + if Config.OIDC_GROUPS: + # Get user groups from any of the possible fields + user_groups = userinfo.get('groups', # Generic + userinfo.get('entitlements', # GEANT + userinfo.get('eduperson_entitlement', # EGI Check-in + []))) + + if not set(Config.OIDC_GROUPS).issubset(user_groups): + raise InvaliddUserException("Invalid InfrastructureManager credentials. " + + "User not in configured groups.") + + except Exception as ex: InfrastructureManager.logger.exception("Error trying to validate OIDC auth token: %s" % str(ex)) raise Exception("Error trying to validate OIDC auth token: %s" % str(ex)) @@ -1928,10 +1942,10 @@ def EstimateResouces(radl_data, auth): # If any deploy is defined, only update definitions. if not radl.deploys: - InfrastructureManager.logger.warn("Getting cost of and infrastructure without any deploy.") + InfrastructureManager.logger.warn("Getting cost of an infrastructure without any deploy.") return res except Exception as ex: - InfrastructureManager.logger.exception("Error getting cost of and infrastructure when parsing RADL") + InfrastructureManager.logger.exception("Error getting cost of an infrastructure when parsing RADL") raise ex InfrastructureManager.add_app_reqs(radl, inf.id) @@ -1940,7 +1954,7 @@ def EstimateResouces(radl_data, auth): try: systems_with_iis = InfrastructureManager.systems_with_iis(inf, radl, auth) except Exception as ex: - InfrastructureManager.logger.exception("Error getting cost of and infrastructure error getting VM images") + InfrastructureManager.logger.exception("Error getting cost of an infrastructure error getting VM images") raise ex # Concrete systems with cloud providers and select systems with the greatest score @@ -1956,7 +1970,7 @@ def EstimateResouces(radl_data, auth): deploy_items = [] for deploy_group in deploy_groups: if not deploy_group: - InfrastructureManager.logger.warning("Error getting cost of and infrastructure: No VMs to deploy!") + InfrastructureManager.logger.warning("Error getting cost of an infrastructure: No VMs to deploy!") cloud_id = deploys_group_cloud[id(deploy_group)] cloud = cloud_list[cloud_id] @@ -1971,7 +1985,7 @@ def EstimateResouces(radl_data, auth): concrete_system = concrete_systems[cloud_id][deploy.id][0] if not concrete_system: - InfrastructureManager.logger.warn("Error getting cost of and infrastructure:" + + InfrastructureManager.logger.warn("Error getting cost of an infrastructure:" + "Error, no concrete system to deploy: " + deploy.id + " in cloud: " + cloud_id + ". Check if a correct image is being used") diff --git a/IM/config.py b/IM/config.py index 18249f272..c259ceb37 100644 --- a/IM/config.py +++ b/IM/config.py @@ -107,6 +107,7 @@ class Config: OIDC_SCOPES = [] OIDC_USER_INFO_PATH = "/userinfo" OIDC_INSTROSPECT_PATH = "/introspect" + OIDC_GROUPS = [] VM_NUM_USE_CTXT_DIST = 30 DELAY_BETWEEN_VM_RETRIES = 5 VERIFI_SSL = False diff --git a/doc/source/manual.rst b/doc/source/manual.rst index bf1fe471a..aa13e5840 100644 --- a/doc/source/manual.rst +++ b/doc/source/manual.rst @@ -507,6 +507,13 @@ OPENID CONNECT OPTIONS Client ID and Secret must be provided to make it work. The default value is ``''``. +.. confval:: OIDC_GROUPS + + List of OIDC groups supported. + It must be a coma separated string of group names. + (see the `AARC guidelines for group names `_). + The default value is ``''``. + .. confval:: FORCE_OIDC_AUTH If ``True`` the IM will force the users to pass a valid OIDC token. diff --git a/etc/im.cfg b/etc/im.cfg index a6525ae4f..e84f33c85 100644 --- a/etc/im.cfg +++ b/etc/im.cfg @@ -146,6 +146,8 @@ OIDC_ISSUERS = https://aai.egi.eu/auth/realms/egi # Paths to the userinfo and introspection OIDC #OIDC_USER_INFO_PATH = "/userinfo" #OIDC_INSTROSPECT_PATH = "/introspect" +# List of OIDC groups that will be allowed to access the IM service +#OIDC_GROUPS = # Force the users to pass a valid OIDC token #FORCE_OIDC_AUTH = False diff --git a/test/files/iam_user_info.json b/test/files/iam_user_info.json index 7bc8b105c..848fde0ea 100644 --- a/test/files/iam_user_info.json +++ b/test/files/iam_user_info.json @@ -6,15 +6,10 @@ "email": "", "email_verified": true, "phone_number_verified": false, - "groups": [ - { - "id": "gid", - "name": "Users" - }, - { - "id": "gid", - "name": "Developers" - } + "eduperson_entitlement": [ + "urn:mace:egi.eu:group:demo.fedcloud.egi.eu:members:role=member#aai.egi.eu", + "urn:mace:egi.eu:group:demo.fedcloud.egi.eu:role=member#aai.egi.eu", + "urn:mace:egi.eu:group:demo.fedcloud.egi.eu:vm_operator:role=member#aai.egi.eu" ], "organisation_name": "indigo-dc" } \ No newline at end of file diff --git a/test/unit/test_im_logic.py b/test/unit/test_im_logic.py index 18fc3d86b..c8a2f461f 100644 --- a/test/unit/test_im_logic.py +++ b/test/unit/test_im_logic.py @@ -135,7 +135,7 @@ def get_cloud_connector_mock(self, name="MyMock0"): return cloud @staticmethod - def gen_token(aud=None, exp=None, user_sub="user_sub"): + def gen_token(aud=None, exp=None, user_sub="user_sub", groups=None): data = { "sub": user_sub, "iss": "https://iam-test.indigo-datacloud.eu/", @@ -147,6 +147,8 @@ def gen_token(aud=None, exp=None, user_sub="user_sub"): data["aud"] = aud if exp: data["exp"] = int(time.time()) + exp + if groups: + data["groups"] = groups return ("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.%s.ignored" % base64.urlsafe_b64encode(json.dumps(data).encode("utf-8")).decode("utf-8")) @@ -1126,6 +1128,32 @@ def test_check_oidc_valid_token(self, openidclient): self.assertEqual(im_auth['username'], InfrastructureInfo.OPENID_USER_PREFIX + "micafer") self.assertEqual(im_auth['password'], "https://iam-test.indigo-datacloud.eu/sub") + @patch('IM.InfrastructureManager.OpenIDClient') + def test_check_oidc_groups(self, openidclient): + im_auth = {"token": (self.gen_token())} + + user_info = json.loads(read_file_as_string('../files/iam_user_info.json')) + + openidclient.is_access_token_expired.return_value = False, "Valid Token for 100 seconds" + openidclient.get_user_info_request.return_value = True, user_info + + Config.OIDC_ISSUERS = ["https://iam-test.indigo-datacloud.eu/"] + Config.OIDC_AUDIENCE = None + Config.OIDC_GROUPS = ["urn:mace:egi.eu:group:demo.fedcloud.egi.eu:role=member#aai.egi.eu"] + + IM.check_oidc_token(im_auth) + + self.assertEqual(im_auth['username'], InfrastructureInfo.OPENID_USER_PREFIX + "micafer") + self.assertEqual(im_auth['password'], "https://iam-test.indigo-datacloud.eu/sub") + + Config.OIDC_GROUPS = ["urn:mace:egi.eu:group:demo.fedcloud.egi.eu:role=INVALID#aai.egi.eu"] + + with self.assertRaises(Exception) as ex: + IM.check_oidc_token(im_auth) + self.assertEqual(str(ex.exception), + "Error trying to validate OIDC auth token: Invalid InfrastructureManager" + + " credentials. User not in configured groups.") + def test_inf_auth_with_token(self): im_auth = {"token": (self.gen_token())} im_auth['username'] = InfrastructureInfo.OPENID_USER_PREFIX + "micafer"