diff --git a/src/DIRAC/Core/Security/IAMService.py b/src/DIRAC/Core/Security/IAMService.py index 0748aa81ad6..7796f0be492 100644 --- a/src/DIRAC/Core/Security/IAMService.py +++ b/src/DIRAC/Core/Security/IAMService.py @@ -53,37 +53,52 @@ def __init__(self, access_token, vo=None, forceNickname=False): self.userDict = None self.access_token = access_token self.iam_users_raw = [] + self.iam_groups_raw = [] + + def _getIamPagedResources(self, url): + """Get all items from IAM that are served on a paged endpoint""" + all_items = [] + headers = {"Authorization": f"Bearer {self.access_token}"} + startIndex = 1 + # These are just initial values, they are updated + # while we loop to their actual values + totalResults = 1000 # total number of users + itemsPerPage = 10 + while startIndex <= totalResults: + resp = requests.get(url, headers=headers, params={"startIndex": startIndex}) + resp.raise_for_status() + data = resp.json() + # These 2 should never change while looping + # but you may have a new user appearing + # while looping + totalResults = data["totalResults"] + itemsPerPage = data["itemsPerPage"] + + startIndex += itemsPerPage + all_items.extend(data["Resources"]) + return all_items def _getIamUserDump(self): """List the users from IAM""" - if not self.iam_users_raw: - headers = {"Authorization": f"Bearer {self.access_token}"} - iam_list_url = f"{self.iam_url}/scim/Users" - startIndex = 1 - # These are just initial values, they are updated - # while we loop to their actual values - totalResults = 1000 # total number of users - itemsPerPage = 10 - while startIndex <= totalResults: - resp = requests.get(iam_list_url, headers=headers, params={"startIndex": startIndex}) - resp.raise_for_status() - data = resp.json() - # These 2 should never change while looping - # but you may have a new user appearing - # while looping - totalResults = data["totalResults"] - itemsPerPage = data["itemsPerPage"] - - startIndex += itemsPerPage - self.iam_users_raw.extend(data["Resources"]) + iam_users_url = f"{self.iam_url}/scim/Users" + self.iam_users_raw = self._getIamPagedResources(iam_users_url) return self.iam_users_raw - def convert_iam_to_voms(self, iam_output): + def _getIamGroupDump(self): + """List the groups from IAM""" + if not self.iam_groups_raw: + iam_group_url = f"{self.iam_url}/scim/Groups" + self.iam_groups_raw = self._getIamPagedResources(iam_group_url) + return self.iam_groups_raw + + def convert_iam_to_voms(self, iam_user, iam_voms_groups): """Convert an IAM entry into the voms style, i.e. DN based""" converted_output = {} - for cert in iam_output["urn:indigo-dc:scim:schemas:IndigoUser"]["certificates"]: + certificates = iam_user["urn:indigo-dc:scim:schemas:IndigoUser"]["certificates"] + + for cert in certificates: cert_dict = {} dn = convert_dn(cert["subjectDn"]) ca = convert_dn(cert["issuerDn"]) @@ -96,46 +111,86 @@ def convert_iam_to_voms(self, iam_output): try: cert_dict["nickname"] = [ attr["value"] - for attr in iam_output["urn:indigo-dc:scim:schemas:IndigoUser"]["attributes"] + for attr in iam_user["urn:indigo-dc:scim:schemas:IndigoUser"]["attributes"] if attr["name"] == "nickname" ][0] except (KeyError, IndexError): if not self.forceNickname: - cert_dict["nickname"] = iam_output["userName"] + cert_dict["nickname"] = iam_user["userName"] # This is not correct, we take the overall status instead of the certificate one # however there are no known case of cert suspended while the user isn't - cert_dict["certSuspended"] = not iam_output["active"] + cert_dict["certSuspended"] = not iam_user["active"] # There are still bugs in IAM regarding the active status vs voms suspended - cert_dict["suspended"] = not iam_output["active"] + cert_dict["suspended"] = not iam_user["active"] # The mail may be different, in particular for robot accounts - cert_dict["mail"] = iam_output["emails"][0]["value"].lower() + cert_dict["mail"] = iam_user["emails"][0]["value"].lower() # https://github.com/indigo-iam/voms-importer/blob/main/vomsimporter.py roles = [] - for role in iam_output["groups"]: - role_name = role["display"] - if "/" in role_name: - role_name = role_name.replace("/", "/Role=") - roles.append(f"/{role_name}") + for group in iam_user.get("groups", []): + # ignore non-voms-role groups + if group["value"] not in iam_voms_groups: + continue + + group_name = group["display"] + + # filter also by selected vo + if self.vo is not None and group_name.partition("/")[0] != self.vo: + continue + + role_name = IAMService._group_name_to_role_string(group_name) + roles.append(role_name) + + if len(roles) == 0: + raise ValueError("User must have at least one voms role") cert_dict["Roles"] = roles converted_output[dn] = cert_dict return converted_output + @staticmethod + def _group_name_to_role_string(group_name): + parts = group_name.split("/") + # last part is the role name, need to add Role= + parts[-1] = f"Role={parts[-1]}" + return "/" + "/".join(parts) + + @staticmethod + def _is_voms_role(group): + # labels is returned also with value None, so we cannot simply do get("labels", []) + labels = group.get("urn:indigo-dc:scim:schemas:IndigoGroup", {}).get("labels") + if labels is None: + return False + + for label in labels: + if label["name"] == "voms.role": + return True + + return False + + @staticmethod + def _filter_voms_groups(groups): + return [g for g in groups if IAMService._is_voms_role(g)] + def getUsers(self): """Extract users from IAM user dump. :return: dictionary of: "Users": user dictionary keyed by the user DN, "Errors": list of error messages """ - self.iam_users_raw = self._getIamUserDump() + iam_users_raw = self._getIamUserDump() + all_groups = self._getIamGroupDump() + + voms_groups = self._filter_voms_groups(all_groups) + groups_by_id = {g["id"] for g in voms_groups} + users = {} errors = [] - for user in self.iam_users_raw: + for user in iam_users_raw: try: - users.update(self.convert_iam_to_voms(user)) + users.update(self.convert_iam_to_voms(user, groups_by_id)) except Exception as e: errors.append(f"{user['name']} {e!r}") self.log.error("Could not convert", f"{user['name']} {e!r}") diff --git a/src/DIRAC/Core/Security/test/iam_data.json b/src/DIRAC/Core/Security/test/iam_data.json new file mode 100644 index 00000000000..696ecba5c93 --- /dev/null +++ b/src/DIRAC/Core/Security/test/iam_data.json @@ -0,0 +1,854 @@ +{ + "users": [ + { + "id": "73f16d93-2441-4a50-88ff-85360d78c6b5", + "meta": { + "created": "2026-03-25T18:22:14.000Z", + "lastModified": "2026-03-25T18:22:14.000Z", + "location": "https://iam.test.example/scim/Users/73f16d93-2441-4a50-88ff-85360d78c6b5", + "resourceType": "User" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:User", + "urn:indigo-dc:scim:schemas:IndigoUser" + ], + "userName": "admin", + "name": { + "familyName": "User", + "formatted": "Admin User", + "givenName": "Admin" + }, + "displayName": "admin", + "active": true, + "emails": [ + { + "type": "work", + "value": "admin@iam.test", + "primary": true + } + ], + "urn:indigo-dc:scim:schemas:IndigoUser": {} + }, + { + "id": "c2e241bd-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:46.000Z", + "lastModified": "2026-03-25T18:23:46.000Z", + "location": "https://iam.test.example/scim/Users/c2e241bd-2877-11f1-8d29-364a3910c199", + "resourceType": "User" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:User", + "urn:indigo-dc:scim:schemas:IndigoUser" + ], + "userName": "test-user", + "name": { + "familyName": "User", + "formatted": "TestDpps User", + "givenName": "TestDpps" + }, + "displayName": "test-user", + "active": true, + "emails": [ + { + "type": "work", + "value": "dpps@test.example", + "primary": true + } + ], + "groups": [ + { + "display": "ctao.dpps.test", + "value": "c2c8a08e-2877-11f1-8d29-364a3910c199", + "$ref": "https://iam.test.example/scim/Groups/c2c8a08e-2877-11f1-8d29-364a3910c199" + }, + { + "display": "ctao.dpps.test/dpps/user", + "value": "c2c9195b-2877-11f1-8d29-364a3910c199", + "$ref": "https://iam.test.example/scim/Groups/c2c9195b-2877-11f1-8d29-364a3910c199" + }, + { + "display": "ctao.dpps.test/dpps/pipelines/user", + "value": "c2c9b71a-2877-11f1-8d29-364a3910c199", + "$ref": "https://iam.test.example/scim/Groups/c2c9b71a-2877-11f1-8d29-364a3910c199" + }, + { + "display": "ctao.dpps.test/dpps/archive/user", + "value": "c2c9e1e3-2877-11f1-8d29-364a3910c199", + "$ref": "https://iam.test.example/scim/Groups/c2c9e1e3-2877-11f1-8d29-364a3910c199" + }, + { + "display": "ctao.dpps.test/dpps/dataquality/user", + "value": "c2ca135c-2877-11f1-8d29-364a3910c199", + "$ref": "https://iam.test.example/scim/Groups/c2ca135c-2877-11f1-8d29-364a3910c199" + } + ], + "urn:indigo-dc:scim:schemas:IndigoUser": { + "certificates": [ + { + "primary": true, + "subjectDn": "CN=DPPS User", + "issuerDn": "CN=DPPS Development CA", + "pemEncodedCertificate": "-----BEGIN CERTIFICATE-----\nMIIDFDCCAfygAwIBAgIUWSpm66BDmQ5S3F5islg8y3DJvfcwDQYJKoZIhvcNAQEL\nBQAwHjEcMBoGA1UEAwwTRFBQUyBEZXZlbG9wbWVudCBDQTAgFw0yNjAzMjUxODIx\nMjJaGA8yMDUwMTExNDE4MjEyMlowFDESMBAGA1UEAwwJRFBQUyBVc2VyMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsdoSQbF1528f0TvM0myNSVQGRsl3\n1gAhdITyyC3cssKoaQZDZDKWI5x8rxRasTbcs/k6ryKiyD8OyDaxXLqc00ojytv0\ngnaare823RgGlAbUsr/sBOxb4EY/mnpPtjhTzRU0EAdImBtAyjPcbbTjotbLRdR2\nMdCBIobJwAzMxVUk9TWYknKRQqFRwxCmuA6+P4NphGE0wrae4sak+VAyOqBIqUqk\n9kre7BDopQHhOc3pE/ekka+x70UIbMWOuOpK+kk8kZqkzJcAmBgm6GqBA1cZbTKh\nIl52RigLBFFQBLbowWYjwJNdVRkQlxHplZJkNBRz/UWu6qL0PikEBIkB/QIDAQAB\no1IwUDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0OBBYEFIiOvfe/pr7T8Y5532bLN1xn\nO0anMB8GA1UdIwQYMBaAFPJEl9u6MM6EDJjrKET3PrRcWd+oMA0GCSqGSIb3DQEB\nCwUAA4IBAQAFzTP752/J0TwsGBgxqzwaM0EvS9W6kHO1LulFPlrO0KfT6jSOzq0C\npCdXwFHBeOw+3cyHyr8EKeK2GkxG0C3kQJmjQY5PTEwwlFiAY03SiviJhnYsp21t\nWyQuoWjL0lX2bZJK0gY3F/mibVMIH8ECfDVf9LrSgchBaO5UCRA+0zN72ZNXoxw2\nCaMXETqh2CAMb4V+wXFwZtwdzMyc84CMhYOKxlLFu+pzWLcITDWjmlAOJwT7rFPJ\n0DYNzmDxijuMcNz9mHP5hJxzb+RjjPDsiOHFwNYLCYqm8VkJ/gU8bqtNaSgcLgTU\nw0iRty3lUF3m7B62LQz53KybhRmrnmJh\n-----END CERTIFICATE-----\n", + "display": "test-user cert", + "created": "2026-03-25T18:23:46.000Z", + "lastModified": "2026-03-25T18:23:46.000Z", + "hasProxyCertificate": false + } + ] + } + }, + { + "id": "c2faf772-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:46.000Z", + "lastModified": "2026-03-25T18:23:46.000Z", + "location": "https://iam.test.example/scim/Users/c2faf772-2877-11f1-8d29-364a3910c199", + "resourceType": "User" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:User", + "urn:indigo-dc:scim:schemas:IndigoUser" + ], + "userName": "test-user-unprivileged", + "name": { + "familyName": "User", + "formatted": "TestUnprivilegedDpps User", + "givenName": "TestUnprivilegedDpps" + }, + "displayName": "test-user-unprivileged", + "active": true, + "emails": [ + { + "type": "work", + "value": "unprivileged-dpps@test.example", + "primary": true + } + ], + "groups": [ + { + "display": "alt_ctao.dpps.test", + "value": "c2fbf3ed-2877-11f1-8d29-364a3910c199", + "$ref": "https://iam.test.example/scim/Groups/c2fbf3ed-2877-11f1-8d29-364a3910c199" + } + ], + "urn:indigo-dc:scim:schemas:IndigoUser": { + "certificates": [ + { + "primary": true, + "subjectDn": "CN=DPPS User Unprivileged", + "issuerDn": "CN=DPPS Development CA", + "pemEncodedCertificate": "-----BEGIN CERTIFICATE-----\nMIIDITCCAgmgAwIBAgIUWSpm66BDmQ5S3F5islg8y3DJvfgwDQYJKoZIhvcNAQEL\nBQAwHjEcMBoGA1UEAwwTRFBQUyBEZXZlbG9wbWVudCBDQTAgFw0yNjAzMjUxODIx\nMjNaGA8yMDUwMTExNDE4MjEyM1owITEfMB0GA1UEAwwWRFBQUyBVc2VyIFVucHJp\ndmlsZWdlZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKOCSd/opqET\nlM8tyKbi9t92/1AIMusbdJs8LwR1v+VfugemxtUU0VcMkb5/8i++ey7UJ/Rdj/1S\n9DqSeglZ+7Mr3Qf6c56vo5SFCWOYsxILXFqosi3hCijGpCIww7yaJR8YgpS1xPLx\nu+Lml9pQbZD/11kuGTMUQRZEtiqWtO58GJRW2WRZoofkHGn7+oQgVi6XrK2o/77I\nZ2WSf1eGra/AKP9PyVghBAOC2Sancjqmh3z8TYBHQiIQA/8Ef+BXwj9+QecP2+5X\ndkoq+dZhmdHBwU87+mT7vYbRfGNAOAyiCIwSk8ChqZXLedh6RtJfIbSnxZfdHVuD\n7pxW9qRiawsCAwEAAaNSMFAwDgYDVR0PAQH/BAQDAgWgMB0GA1UdDgQWBBShLiEa\nd3IgXbfaaFZM8m+Sa5urzTAfBgNVHSMEGDAWgBTyRJfbujDOhAyY6yhE9z60XFnf\nqDANBgkqhkiG9w0BAQsFAAOCAQEAhEG2PawoXSRVqdXAZ1J083WazLdlMEFwIbO8\nKwuf7MQ6RaJGABjkIGu3nC10zV12MLhLEUSU8376HGXXuTha0UGKhL8ESLQJHpy+\n4DNkRVARz5qgGl26BE+r6m1rl7n+6Q7kj191OjYc1BeVAR3iQ04+GYqzOo5YgDWh\nxxKCvX9uLv0XAbXV7WvoDpOUMQlhof45ADCKciOfhq5k/tvtyywFHHZw2Ixi7PYc\n2tYqya9drvYw+Fkx4D2OthtHkE2V3lmtHyRpkT3H5NuotyB0cmuMrtRSgRbaj6UK\nTyIR+/UTELgoNLriv2xeufEucysh+9U7WsQnKPrnh12MkIRvAg==\n-----END CERTIFICATE-----\n", + "display": "test-user-unprivileged cert", + "created": "2026-03-25T18:23:46.000Z", + "lastModified": "2026-03-25T18:23:46.000Z", + "hasProxyCertificate": false + } + ] + } + }, + { + "id": "c31479ae-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:46.000Z", + "lastModified": "2026-03-25T18:23:46.000Z", + "location": "https://iam.test.example/scim/Users/c31479ae-2877-11f1-8d29-364a3910c199", + "resourceType": "User" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:User", + "urn:indigo-dc:scim:schemas:IndigoUser" + ], + "userName": "test-user-non-dpps", + "name": { + "familyName": "User", + "formatted": "TestNonDpps User", + "givenName": "TestNonDpps" + }, + "displayName": "test-user-non-dpps", + "active": true, + "emails": [ + { + "type": "work", + "value": "non-dpps@test.example", + "primary": true + } + ], + "urn:indigo-dc:scim:schemas:IndigoUser": { + "certificates": [ + { + "primary": true, + "subjectDn": "CN=Non-DPPS User", + "issuerDn": "CN=DPPS Development CA", + "pemEncodedCertificate": "-----BEGIN CERTIFICATE-----\nMIIDGDCCAgCgAwIBAgIUWSpm66BDmQ5S3F5islg8y3DJvfkwDQYJKoZIhvcNAQEL\nBQAwHjEcMBoGA1UEAwwTRFBQUyBEZXZlbG9wbWVudCBDQTAgFw0yNjAzMjUxODIx\nMjNaGA8yMDUwMTExNDE4MjEyM1owGDEWMBQGA1UEAwwNTm9uLURQUFMgVXNlcjCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKtok+yBFBXw2Llz31eHv13e\nws+y0GTG75anhkJ78DBVyrbCXYY9CuGsXFuMDFEvzYtCf6+rCyR9HHkMLurbRQOO\nuoVcLymZ5ty4lepv8p3u4/KVdvwFPODsY0drJyGCKcPahp/JIFzNKnr5vyV+NOUa\nYyjRKAO44k+SJWmzap8YI9/IsXvTyCejdyTX6MMzVNB5vlILFSZhn/VJLbkXOKAQ\nriqMo+RjhaLG4NfzfOVTDHX2qmLkFGkj6ReNpEm1z9y1pzmYg52qH7d0kZ9Kpl47\n5FThf4lvfU4tu+2pcunrFkoa5xyemBmfJpiaQVZqNJ3Cdmh3+oQTS/VdpiaLDgUC\nAwEAAaNSMFAwDgYDVR0PAQH/BAQDAgWgMB0GA1UdDgQWBBRfXrotnG53/9r4iwMO\nAbvdaBUedzAfBgNVHSMEGDAWgBTyRJfbujDOhAyY6yhE9z60XFnfqDANBgkqhkiG\n9w0BAQsFAAOCAQEANDPrAef9+sWUqti21MwcnJ+zAnU2eY32tcUUvO+JsDdqDL/U\nlJCfAMwmHaZpmRj3G0pzvUKIlgHdRKvUaLPhTZovwaZ2ftwBHVBWSM5J+oY0JYzp\nvnCyIs//7/eetkZVoUt5+armtmmtg7zL06ZN9sUeHoadJCQ07Q5MGCoQuc0NpcUx\n1pgQk0fckgJCr4mNlBz0PGu291vYOKDpK0tICvQIaG9f2Cu7UBRiIGaIyjeypGUr\n9Il/XdGtCJ72GB4EvkWSBD8zi/p/icCX5ISYP++bpgjtiDx7UplNul9C8LsFraHr\na4PUfGO6vgi/J+oQ6f5nHI5TnCSrdVTZcSufLw==\n-----END CERTIFICATE-----\n", + "display": "test-user-non-dpps cert", + "created": "2026-03-25T18:23:46.000Z", + "lastModified": "2026-03-25T18:23:46.000Z", + "hasProxyCertificate": false + } + ] + } + }, + { + "id": "c32c9b29-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:46.000Z", + "lastModified": "2026-03-25T18:23:46.000Z", + "location": "https://iam.test.example/scim/Users/c32c9b29-2877-11f1-8d29-364a3910c199", + "resourceType": "User" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:User", + "urn:indigo-dc:scim:schemas:IndigoUser" + ], + "userName": "admin-user", + "name": { + "familyName": "User", + "formatted": "TestAdmin User", + "givenName": "TestAdmin" + }, + "displayName": "admin-user", + "active": true, + "emails": [ + { + "type": "work", + "value": "dpps@test.example", + "primary": true + } + ], + "groups": [ + { + "display": "ctao.dpps.test", + "value": "c2c8a08e-2877-11f1-8d29-364a3910c199", + "$ref": "https://iam.test.example/scim/Groups/c2c8a08e-2877-11f1-8d29-364a3910c199" + }, + { + "display": "ctao.dpps.test/dpps/archive/metadata-manager", + "value": "c2ca030f-2877-11f1-8d29-364a3910c199", + "$ref": "https://iam.test.example/scim/Groups/c2ca030f-2877-11f1-8d29-364a3910c199" + }, + { + "display": "ctao.dpps.test/dpps/pipelines/user", + "value": "c2c9b71a-2877-11f1-8d29-364a3910c199", + "$ref": "https://iam.test.example/scim/Groups/c2c9b71a-2877-11f1-8d29-364a3910c199" + }, + { + "display": "ctao.dpps.test/dpps/pipelines/manager", + "value": "c2c9ceea-2877-11f1-8d29-364a3910c199", + "$ref": "https://iam.test.example/scim/Groups/c2c9ceea-2877-11f1-8d29-364a3910c199" + }, + { + "display": "ctao.dpps.test/dpps/archive/manager", + "value": "c2c9ed94-2877-11f1-8d29-364a3910c199", + "$ref": "https://iam.test.example/scim/Groups/c2c9ed94-2877-11f1-8d29-364a3910c199" + }, + { + "display": "ctao.dpps.test/dpps/dataquality/user", + "value": "c2ca135c-2877-11f1-8d29-364a3910c199", + "$ref": "https://iam.test.example/scim/Groups/c2ca135c-2877-11f1-8d29-364a3910c199" + }, + { + "display": "ctao.dpps.test/dpps/manager", + "value": "c2c99745-2877-11f1-8d29-364a3910c199", + "$ref": "https://iam.test.example/scim/Groups/c2c99745-2877-11f1-8d29-364a3910c199" + }, + { + "display": "ctao.dpps.test/dpps/user", + "value": "c2c9195b-2877-11f1-8d29-364a3910c199", + "$ref": "https://iam.test.example/scim/Groups/c2c9195b-2877-11f1-8d29-364a3910c199" + }, + { + "display": "ctao.dpps.test/dpps/archive/user", + "value": "c2c9e1e3-2877-11f1-8d29-364a3910c199", + "$ref": "https://iam.test.example/scim/Groups/c2c9e1e3-2877-11f1-8d29-364a3910c199" + }, + { + "display": "ctao.dpps.test/dpps/dataquality/manager", + "value": "c2ca1d3e-2877-11f1-8d29-364a3910c199", + "$ref": "https://iam.test.example/scim/Groups/c2ca1d3e-2877-11f1-8d29-364a3910c199" + } + ], + "urn:indigo-dc:scim:schemas:IndigoUser": {} + }, + { + "id": "c3452356-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:46.000Z", + "lastModified": "2026-03-25T18:23:46.000Z", + "location": "https://iam.test.example/scim/Users/c3452356-2877-11f1-8d29-364a3910c199", + "resourceType": "User" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:User", + "urn:indigo-dc:scim:schemas:IndigoUser" + ], + "userName": "test-user-non-ctao", + "name": { + "familyName": "User", + "formatted": "TestNonCTAO User", + "givenName": "TestNonCTAO" + }, + "displayName": "test-user-non-ctao", + "active": true, + "emails": [ + { + "type": "work", + "value": "non-ctao@test.example", + "primary": true + } + ], + "groups": [ + { + "display": "non-ctao.test", + "value": "c3454859-2877-11f1-8d29-364a3910c199", + "$ref": "https://iam.test.example/scim/Groups/c3454859-2877-11f1-8d29-364a3910c199" + } + ], + "urn:indigo-dc:scim:schemas:IndigoUser": { + "certificates": [ + { + "primary": true, + "subjectDn": "CN=Non-CTAO User", + "issuerDn": "CN=DPPS Development CA", + "pemEncodedCertificate": "-----BEGIN CERTIFICATE-----\nMIIDGDCCAgCgAwIBAgIUWSpm66BDmQ5S3F5islg8y3DJvfowDQYJKoZIhvcNAQEL\nBQAwHjEcMBoGA1UEAwwTRFBQUyBEZXZlbG9wbWVudCBDQTAgFw0yNjAzMjUxODIx\nMjNaGA8yMDUwMTExNDE4MjEyM1owGDEWMBQGA1UEAwwNTm9uLUNUQU8gVXNlcjCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL2njE2nEZPpa7Fx/ZkmEhYV\nj0+CCcv6v52pZf/1qbDnYE/q84ceDL179YQkPRVG8PPMRxxosfTRVYJ2IYwdS0n2\nzoNpt73sXgxNLkvVqnqqnrK3m7zp1b2PvCVNCz782ZfgCcjRyBFx8Zt648scE63P\nRjByCIW4YHY5U4DlRq+sIWgJHHe62IqPaVXkwB6dL0Pw++H7KMfasGBlQjkLR2Eb\nMuhMIA3mVi3xu6wdoRcqV0AUHRY/C/L8tv3x2m3tFmv75ty50Uf63jua3fRJlQhY\niJUlzWLEslLjRaRWuKHrptO+0iqU5LLr7kvFcbLWRa60EdpJ/ceIfYkt1Zr7+O8C\nAwEAAaNSMFAwDgYDVR0PAQH/BAQDAgWgMB0GA1UdDgQWBBRWQF2BvYfO9A5ipCOH\nLXOEUIyTEDAfBgNVHSMEGDAWgBTyRJfbujDOhAyY6yhE9z60XFnfqDANBgkqhkiG\n9w0BAQsFAAOCAQEAWFiL5fCk/m+HPMo1A7kMr8zFjuAGp5Lnd57VlMzzJGI+sq+f\nMGnm/af2QXKig4hsl90B5aFOy6Fb0tQuBncq4h2jC9aL96HHM47MiuZoXV+UtBOK\nVKGz7fxoR1EdU8ljeYKRu6919/clXmgiE/i48D2y71d8OTGT56dwZz9FmrDgLi8N\nNdCmN/pv3ptQZbQNRTjlisd26YdwOYWtmZzFt3BL13AmUVWdExsaH6d6jJ1qHAKH\nOWVqvljoctTkUR05z1CeOLeGNWL/QPicX1nWwaEachuQlSND/c8A//V1MEJ3wBTs\nDTWS6ADDKMdj43h9YFpJjg/xzgzBK3BsoLZ+Lw==\n-----END CERTIFICATE-----\n", + "display": "test-user-non-ctao cert", + "created": "2026-03-25T18:23:46.000Z", + "lastModified": "2026-03-25T18:23:46.000Z", + "hasProxyCertificate": false + } + ] + } + }, + { + "id": "19e3b6be-2879-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:33:21.000Z", + "lastModified": "2026-03-25T18:33:21.000Z", + "location": "https://iam.test.example/scim/Users/19e3b6be-2879-11f1-8d29-364a3910c199", + "resourceType": "User" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:User", + "urn:indigo-dc:scim:schemas:IndigoUser" + ], + "userName": "dpps-unprivileged-user", + "name": { + "familyName": "User", + "formatted": "DPPS Unprivileged User", + "givenName": "DPPS Unprivileged" + }, + "displayName": "dpps-unprivileged-user", + "active": true, + "emails": [ + { + "type": "work", + "value": "dpps@test.example", + "primary": true + } + ], + "groups": [ + { + "display": "ctao.dpps.test", + "value": "c2c8a08e-2877-11f1-8d29-364a3910c199", + "$ref": "https://iam.test.example/scim/Groups/c2c8a08e-2877-11f1-8d29-364a3910c199" + } + ], + "urn:indigo-dc:scim:schemas:IndigoUser": {} + } + ], + "groups": [ + { + "id": "c2c8a08e-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:45.000Z", + "lastModified": "2026-03-25T18:23:45.000Z", + "location": "https://iam.test.example/scim/Groups/c2c8a08e-2877-11f1-8d29-364a3910c199", + "resourceType": "Group" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "urn:indigo-dc:scim:schemas:IndigoGroup" + ], + "displayName": "ctao.dpps.test", + "urn:indigo-dc:scim:schemas:IndigoGroup": { + "parentGroup": null, + "description": "Group ctao.dpps.test", + "labels": null + } + }, + { + "id": "c2c8a7e0-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:45.000Z", + "lastModified": "2026-03-25T18:23:45.000Z", + "location": "https://iam.test.example/scim/Groups/c2c8a7e0-2877-11f1-8d29-364a3910c199", + "resourceType": "Group" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "urn:indigo-dc:scim:schemas:IndigoGroup" + ], + "displayName": "ctao.dpps.test/dpps", + "urn:indigo-dc:scim:schemas:IndigoGroup": { + "parentGroup": null, + "description": "Group ctao.dpps.test/dpps", + "labels": null + } + }, + { + "id": "c2c9195b-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:45.000Z", + "lastModified": "2026-03-25T18:23:45.000Z", + "location": "https://iam.test.example/scim/Groups/c2c9195b-2877-11f1-8d29-364a3910c199", + "resourceType": "Group" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "urn:indigo-dc:scim:schemas:IndigoGroup" + ], + "displayName": "ctao.dpps.test/dpps/user", + "urn:indigo-dc:scim:schemas:IndigoGroup": { + "parentGroup": null, + "description": "Group ctao.dpps.test/dpps/user", + "labels": [ + { + "prefix": null, + "name": "voms.role", + "value": null + }, + { + "prefix": null, + "name": "wlcg.optional-group", + "value": null + } + ] + } + }, + { + "id": "c2c99745-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:45.000Z", + "lastModified": "2026-03-25T18:23:45.000Z", + "location": "https://iam.test.example/scim/Groups/c2c99745-2877-11f1-8d29-364a3910c199", + "resourceType": "Group" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "urn:indigo-dc:scim:schemas:IndigoGroup" + ], + "displayName": "ctao.dpps.test/dpps/manager", + "urn:indigo-dc:scim:schemas:IndigoGroup": { + "parentGroup": null, + "description": "Group ctao.dpps.test/dpps/manager", + "labels": [ + { + "prefix": null, + "name": "voms.role", + "value": null + }, + { + "prefix": null, + "name": "wlcg.optional-group", + "value": null + } + ] + } + }, + { + "id": "c2c9a585-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:45.000Z", + "lastModified": "2026-03-25T18:23:45.000Z", + "location": "https://iam.test.example/scim/Groups/c2c9a585-2877-11f1-8d29-364a3910c199", + "resourceType": "Group" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "urn:indigo-dc:scim:schemas:IndigoGroup" + ], + "displayName": "ctao.dpps.test/dpps/operator", + "urn:indigo-dc:scim:schemas:IndigoGroup": { + "parentGroup": null, + "description": "Group ctao.dpps.test/dpps/operator", + "labels": [ + { + "prefix": null, + "name": "voms.role", + "value": null + }, + { + "prefix": null, + "name": "wlcg.optional-group", + "value": null + } + ] + } + }, + { + "id": "c2c9b17f-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:45.000Z", + "lastModified": "2026-03-25T18:23:45.000Z", + "location": "https://iam.test.example/scim/Groups/c2c9b17f-2877-11f1-8d29-364a3910c199", + "resourceType": "Group" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "urn:indigo-dc:scim:schemas:IndigoGroup" + ], + "displayName": "ctao.dpps.test/dpps/pipelines", + "urn:indigo-dc:scim:schemas:IndigoGroup": { + "parentGroup": null, + "description": "Group ctao.dpps.test/dpps/pipelines", + "labels": null + } + }, + { + "id": "c2c9b71a-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:45.000Z", + "lastModified": "2026-03-25T18:23:45.000Z", + "location": "https://iam.test.example/scim/Groups/c2c9b71a-2877-11f1-8d29-364a3910c199", + "resourceType": "Group" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "urn:indigo-dc:scim:schemas:IndigoGroup" + ], + "displayName": "ctao.dpps.test/dpps/pipelines/user", + "urn:indigo-dc:scim:schemas:IndigoGroup": { + "parentGroup": null, + "description": "Group ctao.dpps.test/dpps/pipelines/user", + "labels": [ + { + "prefix": null, + "name": "voms.role", + "value": null + }, + { + "prefix": null, + "name": "wlcg.optional-group", + "value": null + } + ] + } + }, + { + "id": "c2c9c2d8-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:45.000Z", + "lastModified": "2026-03-25T18:23:45.000Z", + "location": "https://iam.test.example/scim/Groups/c2c9c2d8-2877-11f1-8d29-364a3910c199", + "resourceType": "Group" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "urn:indigo-dc:scim:schemas:IndigoGroup" + ], + "displayName": "ctao.dpps.test/dpps/pipelines/expert", + "urn:indigo-dc:scim:schemas:IndigoGroup": { + "parentGroup": null, + "description": "Group ctao.dpps.test/dpps/pipelines/expert", + "labels": [ + { + "prefix": null, + "name": "voms.role", + "value": null + }, + { + "prefix": null, + "name": "wlcg.optional-group", + "value": null + } + ] + } + }, + { + "id": "c2c9ceea-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:45.000Z", + "lastModified": "2026-03-25T18:23:45.000Z", + "location": "https://iam.test.example/scim/Groups/c2c9ceea-2877-11f1-8d29-364a3910c199", + "resourceType": "Group" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "urn:indigo-dc:scim:schemas:IndigoGroup" + ], + "displayName": "ctao.dpps.test/dpps/pipelines/manager", + "urn:indigo-dc:scim:schemas:IndigoGroup": { + "parentGroup": null, + "description": "Group ctao.dpps.test/dpps/pipelines/manager", + "labels": [ + { + "prefix": null, + "name": "voms.role", + "value": null + }, + { + "prefix": null, + "name": "wlcg.optional-group", + "value": null + } + ] + } + }, + { + "id": "c2c9dbb7-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:45.000Z", + "lastModified": "2026-03-25T18:23:45.000Z", + "location": "https://iam.test.example/scim/Groups/c2c9dbb7-2877-11f1-8d29-364a3910c199", + "resourceType": "Group" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "urn:indigo-dc:scim:schemas:IndigoGroup" + ], + "displayName": "ctao.dpps.test/dpps/archive", + "urn:indigo-dc:scim:schemas:IndigoGroup": { + "parentGroup": null, + "description": "Group ctao.dpps.test/dpps/archive", + "labels": null + } + }, + { + "id": "c2c9e1e3-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:45.000Z", + "lastModified": "2026-03-25T18:23:45.000Z", + "location": "https://iam.test.example/scim/Groups/c2c9e1e3-2877-11f1-8d29-364a3910c199", + "resourceType": "Group" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "urn:indigo-dc:scim:schemas:IndigoGroup" + ], + "displayName": "ctao.dpps.test/dpps/archive/user", + "urn:indigo-dc:scim:schemas:IndigoGroup": { + "parentGroup": null, + "description": "Group ctao.dpps.test/dpps/archive/user", + "labels": [ + { + "prefix": null, + "name": "voms.role", + "value": null + }, + { + "prefix": null, + "name": "wlcg.optional-group", + "value": null + } + ] + } + }, + { + "id": "c2c9ed94-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:45.000Z", + "lastModified": "2026-03-25T18:23:45.000Z", + "location": "https://iam.test.example/scim/Groups/c2c9ed94-2877-11f1-8d29-364a3910c199", + "resourceType": "Group" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "urn:indigo-dc:scim:schemas:IndigoGroup" + ], + "displayName": "ctao.dpps.test/dpps/archive/manager", + "urn:indigo-dc:scim:schemas:IndigoGroup": { + "parentGroup": null, + "description": "Group ctao.dpps.test/dpps/archive/manager", + "labels": [ + { + "prefix": null, + "name": "voms.role", + "value": null + }, + { + "prefix": null, + "name": "wlcg.optional-group", + "value": null + } + ] + } + }, + { + "id": "c2c9f89b-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:45.000Z", + "lastModified": "2026-03-25T18:23:45.000Z", + "location": "https://iam.test.example/scim/Groups/c2c9f89b-2877-11f1-8d29-364a3910c199", + "resourceType": "Group" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "urn:indigo-dc:scim:schemas:IndigoGroup" + ], + "displayName": "ctao.dpps.test/dpps/archive/producer", + "urn:indigo-dc:scim:schemas:IndigoGroup": { + "parentGroup": null, + "description": "Group ctao.dpps.test/dpps/archive/producer", + "labels": [ + { + "prefix": null, + "name": "voms.role", + "value": null + }, + { + "prefix": null, + "name": "wlcg.optional-group", + "value": null + } + ] + } + }, + { + "id": "c2ca030f-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:45.000Z", + "lastModified": "2026-03-25T18:23:45.000Z", + "location": "https://iam.test.example/scim/Groups/c2ca030f-2877-11f1-8d29-364a3910c199", + "resourceType": "Group" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "urn:indigo-dc:scim:schemas:IndigoGroup" + ], + "displayName": "ctao.dpps.test/dpps/archive/metadata-manager", + "urn:indigo-dc:scim:schemas:IndigoGroup": { + "parentGroup": null, + "description": "Group ctao.dpps.test/dpps/archive/metadata-manager", + "labels": [ + { + "prefix": null, + "name": "voms.role", + "value": null + }, + { + "prefix": null, + "name": "wlcg.optional-group", + "value": null + } + ] + } + }, + { + "id": "c2ca0dbc-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:45.000Z", + "lastModified": "2026-03-25T18:23:45.000Z", + "location": "https://iam.test.example/scim/Groups/c2ca0dbc-2877-11f1-8d29-364a3910c199", + "resourceType": "Group" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "urn:indigo-dc:scim:schemas:IndigoGroup" + ], + "displayName": "ctao.dpps.test/dpps/dataquality", + "urn:indigo-dc:scim:schemas:IndigoGroup": { + "parentGroup": null, + "description": "Group ctao.dpps.test/dpps/dataquality", + "labels": null + } + }, + { + "id": "c2ca135c-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:45.000Z", + "lastModified": "2026-03-25T18:23:45.000Z", + "location": "https://iam.test.example/scim/Groups/c2ca135c-2877-11f1-8d29-364a3910c199", + "resourceType": "Group" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "urn:indigo-dc:scim:schemas:IndigoGroup" + ], + "displayName": "ctao.dpps.test/dpps/dataquality/user", + "urn:indigo-dc:scim:schemas:IndigoGroup": { + "parentGroup": null, + "description": "Group ctao.dpps.test/dpps/dataquality/user", + "labels": [ + { + "prefix": null, + "name": "voms.role", + "value": null + }, + { + "prefix": null, + "name": "wlcg.optional-group", + "value": null + } + ] + } + }, + { + "id": "c2ca1d3e-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:45.000Z", + "lastModified": "2026-03-25T18:23:45.000Z", + "location": "https://iam.test.example/scim/Groups/c2ca1d3e-2877-11f1-8d29-364a3910c199", + "resourceType": "Group" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "urn:indigo-dc:scim:schemas:IndigoGroup" + ], + "displayName": "ctao.dpps.test/dpps/dataquality/manager", + "urn:indigo-dc:scim:schemas:IndigoGroup": { + "parentGroup": null, + "description": "Group ctao.dpps.test/dpps/dataquality/manager", + "labels": [ + { + "prefix": null, + "name": "voms.role", + "value": null + }, + { + "prefix": null, + "name": "wlcg.optional-group", + "value": null + } + ] + } + }, + { + "id": "c2fbf3ed-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:46.000Z", + "lastModified": "2026-03-25T18:23:46.000Z", + "location": "https://iam.test.example/scim/Groups/c2fbf3ed-2877-11f1-8d29-364a3910c199", + "resourceType": "Group" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "urn:indigo-dc:scim:schemas:IndigoGroup" + ], + "displayName": "alt_ctao.dpps.test", + "urn:indigo-dc:scim:schemas:IndigoGroup": { + "parentGroup": null, + "description": "Group alt_ctao.dpps.test", + "labels": null + } + }, + { + "id": "c3454859-2877-11f1-8d29-364a3910c199", + "meta": { + "created": "2026-03-25T18:23:46.000Z", + "lastModified": "2026-03-25T18:23:46.000Z", + "location": "https://iam.test.example/scim/Groups/c3454859-2877-11f1-8d29-364a3910c199", + "resourceType": "Group" + }, + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "urn:indigo-dc:scim:schemas:IndigoGroup" + ], + "displayName": "non-ctao.test", + "urn:indigo-dc:scim:schemas:IndigoGroup": { + "parentGroup": null, + "description": "Group non-ctao.test", + "labels": null + } + } + ] +} diff --git a/src/DIRAC/Core/Security/test/test_IAMService.py b/src/DIRAC/Core/Security/test/test_IAMService.py new file mode 100644 index 00000000000..abbe4834e4c --- /dev/null +++ b/src/DIRAC/Core/Security/test/test_IAMService.py @@ -0,0 +1,68 @@ +import json +from pathlib import Path + +import pytest + +iam_data = json.loads((Path(__file__).parent / "iam_data.json").read_text()) +groups_by_name = {g["displayName"]: g for g in iam_data["groups"]} + + +@pytest.mark.parametrize( + ("group_name", "role_name"), + [ + ("ctao.dpps.test/user", "/ctao.dpps.test/Role=user"), + ("ctao.dpps.test/dpps/user", "/ctao.dpps.test/dpps/Role=user"), + ("ctao.dpps.test/dpps/pipelines/user", "/ctao.dpps.test/dpps/pipelines/Role=user"), + ("ctao.dpps.test/dpps/pipelines/manager", "/ctao.dpps.test/dpps/pipelines/Role=manager"), + ], +) +def test_group_name_to_role_name(group_name, role_name): + from DIRAC.Core.Security.IAMService import IAMService + + assert IAMService._group_name_to_role_string(group_name) == role_name + + +@pytest.mark.parametrize( + ("group", "expected"), + [ + (groups_by_name["ctao.dpps.test"], False), + (groups_by_name["ctao.dpps.test/dpps"], False), + (groups_by_name["ctao.dpps.test/dpps/user"], True), + (groups_by_name["ctao.dpps.test/dpps/pipelines"], False), + (groups_by_name["ctao.dpps.test/dpps/pipelines/user"], True), + ], +) +def test_is_voms_roles(group, expected): + from DIRAC.Core.Security.IAMService import IAMService + + assert IAMService._is_voms_role(group) == expected + + +def test_users_to_cs(): + from DIRAC.ConfigurationSystem.Client.Config import gConfig + from DIRAC.Core.Security.IAMService import IAMService + + vo = "ctao.dpps.test" + gConfig.setOptionValue("/Resources/IdProviders/dummy/issuer", "https://iam.test.example") + gConfig.setOptionValue(f"/Registry/VO/{vo}/IdProvider", "dummy") + + iam_service = IAMService(access_token="dummy", vo="ctao.dpps.test") + # the class uses caching, this prevents it from actually trying to contact an IAM + iam_service.iam_users_raw = iam_data["users"] + iam_service.iam_groups_raw = iam_data["groups"] + + result = iam_service.getUsers() + assert result["OK"] + value = result["Value"] + assert len(value["Errors"]) == 6 + assert sum(1 for error in value["Errors"] if "User must have at least one voms role" in error) == 3 + assert sum(1 for error in value["Errors"] if "KeyError('certificates')" in error) == 3 + assert len(value["Users"]) == 1 + + dpps_user = value["Users"]["/CN=DPPS User"] + assert set(dpps_user["Roles"]) == { + "/ctao.dpps.test/dpps/Role=user", + "/ctao.dpps.test/dpps/pipelines/Role=user", + "/ctao.dpps.test/dpps/archive/Role=user", + "/ctao.dpps.test/dpps/dataquality/Role=user", + }