From e89508c61b428c98b6171a75bcc9cc6aaf99467a Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Wed, 15 Apr 2026 12:28:24 +0200 Subject: [PATCH 01/16] Add aarc scim schema extensions and start populating the one related to the user. --- .../iam/api/scim/converter/UserConverter.java | 23 ++- .../mw/iam/api/scim/model/ScimAarcUser.java | 167 ++++++++++++++++++ .../iam/api/scim/model/ScimAffiliation.java | 32 ++++ .../mw/iam/api/scim/model/ScimAssurance.java | 33 ++++ .../mw/iam/api/scim/model/ScimConstants.java | 3 +- .../iam/api/scim/model/ScimEntitlement.java | 33 ++++ .../infn/mw/iam/api/scim/model/ScimGroup.java | 2 + .../infn/mw/iam/api/scim/model/ScimUser.java | 32 +++- .../scim/converter/UserConverterTest.java | 11 +- .../mw/iam/persistence/model/IamAccount.java | 3 +- 10 files changed, 322 insertions(+), 17 deletions(-) create mode 100644 iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java create mode 100644 iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAffiliation.java create mode 100644 iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAssurance.java create mode 100644 iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimEntitlement.java diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java index 426e006950..5ebc1154c4 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java @@ -30,6 +30,7 @@ import it.infn.mw.iam.api.scim.model.ScimName; import it.infn.mw.iam.api.scim.model.ScimPhoto; import it.infn.mw.iam.api.scim.model.ScimUser; +import it.infn.mw.iam.config.IamProperties; import it.infn.mw.iam.config.scim.ScimProperties; import it.infn.mw.iam.config.scim.ScimProperties.AttributeDescriptor; import it.infn.mw.iam.config.scim.ScimProperties.LabelDescriptor; @@ -57,20 +58,23 @@ public class UserConverter implements Converter { private final AccountGroupManagerService groupManagerService; - private final ScimProperties properties; + private final ScimProperties scimProperties; + private final IamProperties iamProperties; - public UserConverter(ScimProperties properties, ScimResourceLocationProvider rlp, + public UserConverter(ScimProperties scimProperties, ScimResourceLocationProvider rlp, AddressConverter ac, OidcIdConverter oidc, SshKeyConverter sshc, SamlIdConverter samlc, - X509CertificateConverter x509Iamcc, AccountGroupManagerService groupManagerService) { + X509CertificateConverter x509Iamcc, AccountGroupManagerService groupManagerService, + IamProperties iamProperties) { this.resourceLocationProvider = rlp; - this.properties = properties; + this.scimProperties = scimProperties; this.addressConverter = ac; this.oidcIdConverter = oidc; this.sshKeyConverter = sshc; this.samlIdConverter = samlc; this.x509CertificateIamConverter = x509Iamcc; this.groupManagerService = groupManagerService; + this.iamProperties = iamProperties; } @Override @@ -239,7 +243,10 @@ public ScimUser dtoFromEntity(IamAccount entity) { builder.affiliation(entity.getAffiliation()); } - for (LabelDescriptor ld : properties.getIncludeLabels()) { + builder.voPersonId(entity.getUuid() + "@" + iamProperties.getOrganisation().getName()); + builder.organizationName(iamProperties.getOrganisation().getName()); + + for (LabelDescriptor ld : scimProperties.getIncludeLabels()) { entity.getLabelByPrefixAndName(ld.getPrefix(), ld.getName()) .ifPresent(el -> builder.addLabel(ScimLabel.builder() .withPrefix(el.getPrefix()) @@ -248,7 +255,7 @@ public ScimUser dtoFromEntity(IamAccount entity) { .build())); } - for (AttributeDescriptor ad : properties.getIncludeAttributes()) { + for (AttributeDescriptor ad : scimProperties.getIncludeAttributes()) { entity.getAttributeByName(ad.getName()) .ifPresent(attribute -> builder.addAttribute(ScimAttribute.builder() .withName(attribute.getName()) @@ -256,7 +263,7 @@ public ScimUser dtoFromEntity(IamAccount entity) { .build())); } - if (properties.isIncludeManagedGroups()) { + if (scimProperties.isIncludeManagedGroups()) { groupManagerService.getManagedGroupInfoForAccount(entity) .getManagedGroups() .forEach(mg -> builder.addManagedGroup(ScimGroupRef.builder() @@ -266,7 +273,7 @@ public ScimUser dtoFromEntity(IamAccount entity) { .build())); } - if (properties.isIncludeAuthorities()) { + if (scimProperties.isIncludeAuthorities()) { entity.getAuthorities().forEach(a -> builder.addAuthority(a.getAuthority())); } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java new file mode 100644 index 0000000000..bbe13f7cc8 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java @@ -0,0 +1,167 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package it.infn.mw.iam.api.scim.model; + +import java.util.LinkedList; +import java.util.List; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class ScimAarcUser { + + public enum AARC_USER_SCHEMA { + + // @formatter:off + VOPERSON_ID(ScimConstants.AARC_USER_SCHEMA + ".voPersonId"), + ORGANIZATION_NAME(ScimConstants.AARC_USER_SCHEMA + ".organizationName"), + SCHAC_HOME_ORGANIZATION(ScimConstants.AARC_USER_SCHEMA + ".schacHomeOrganization"), + VO_PERSON_EXTERNAL_AFFILIATIONS(ScimConstants.AARC_USER_SCHEMA + ".voPersonExternalAffiliations"), + ASSURANCE(ScimConstants.AARC_USER_SCHEMA + ".assurance"), + ENTITLEMENTS(ScimConstants.AARC_USER_SCHEMA + ".entitlements"); + // @formatter:on + + private final String text; + + AARC_USER_SCHEMA(String text) { + this.text = text; + } + + @Override + public String toString() { + return text; + } + } + + @NotNull + private final String voPersonId; + + private final String organizationName; + + private final String schacHomeOrganization; + + @NotNull + private final List voPersonExternalAffiliations; + + @Valid + private final List assurance; + + @Valid + private final List entitlements; + + @JsonCreator + private ScimAarcUser(@JsonProperty("voPersonId") String voPersonId, + @JsonProperty("organizationName") String organizationName, + @JsonProperty("schacHomeOrganization") String schacHomeOrganization, + @JsonProperty("voPersonExternalAffiliations") List voPersonExternalAffiliations, + @JsonProperty("assurance") List assurance, + @JsonProperty("entitlements") List entitlements) { + + this.voPersonId = voPersonId; + this.organizationName = organizationName; + this.schacHomeOrganization = schacHomeOrganization; + this.voPersonExternalAffiliations = voPersonExternalAffiliations; + this.assurance = assurance != null ? assurance : new LinkedList<>(); + this.entitlements = entitlements != null ? entitlements : new LinkedList<>(); + } + + private ScimAarcUser(Builder b) { + this.voPersonId = b.voPersonId; + this.organizationName = b.organizationName; + this.schacHomeOrganization = b.schacHomeOrganization; + this.voPersonExternalAffiliations = b.voPersonExternalAffiliations; + this.assurance = b.assurance; + this.entitlements = b.entitlements; + } + + public String getVoPersonId() { + return voPersonId; + } + + public String getOrganizationName() { + return organizationName; + } + + public String getSchacHomeOrganization() { + return schacHomeOrganization; + } + + public List getVoPersonExternalAffiliations() { + return voPersonExternalAffiliations; + } + + public List getAssurance() { + return assurance; + } + + public List getEntitlements() { + return entitlements; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String voPersonId; + + private String organizationName; + private String schacHomeOrganization; + private List voPersonExternalAffiliations; + private List assurance = new LinkedList<>(); + private List entitlements = new LinkedList<>(); + + public Builder voPersonId(String voPersonId) { + this.voPersonId = voPersonId; + return this; + } + + public Builder organizationName(String organizationName) { + this.organizationName = organizationName; + return this; + } + + public Builder schacHomeOrganization(String schacHomeOrganization) { + this.schacHomeOrganization = schacHomeOrganization; + return this; + } + + public Builder voPersonExternalAffiliation(ScimAffiliation affiliation) { + this.voPersonExternalAffiliations.add(affiliation); + return this; + } + + public Builder addAssurance(ScimAssurance assurance) { + this.assurance.add(assurance); + return this; + } + + public Builder addEntitlement(ScimEntitlement entitlement) { + this.entitlements.add(entitlement); + return this; + } + + public ScimAarcUser build() { + return new ScimAarcUser(this); + } + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAffiliation.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAffiliation.java new file mode 100644 index 0000000000..327cd0b2fe --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAffiliation.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package it.infn.mw.iam.api.scim.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ScimAffiliation { + private final String value; + + @JsonCreator + public ScimAffiliation(@JsonProperty("value") String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAssurance.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAssurance.java new file mode 100644 index 0000000000..5ea84aadd9 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAssurance.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package it.infn.mw.iam.api.scim.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ScimAssurance { + + private final String value; + + @JsonCreator + public ScimAssurance(@JsonProperty("value") String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimConstants.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimConstants.java index ecd4a9023f..53d79c45a3 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimConstants.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimConstants.java @@ -20,5 +20,6 @@ public interface ScimConstants { final String SCIM_CONTENT_TYPE = "application/scim+json;charset=UTF-8"; final String INDIGO_USER_SCHEMA = "urn:indigo-dc:scim:schemas:IndigoUser"; final String INDIGO_GROUP_SCHEMA = "urn:indigo-dc:scim:schemas:IndigoGroup"; - + final String AARC_USER_SCHEMA = "urn:geant:aarc-community:scim:schemas:core:1.0:User"; + final String AARC_GROUP_SCHEMA = "urn:geant:aarc-community:scim:schemas:core:1.0:Group"; } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimEntitlement.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimEntitlement.java new file mode 100644 index 0000000000..da8dc32211 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimEntitlement.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package it.infn.mw.iam.api.scim.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ScimEntitlement { + + private final String value; + + @JsonCreator + public ScimEntitlement(@JsonProperty("value") String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java index 60cb02cd13..e167b6d41b 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java @@ -15,6 +15,7 @@ */ package it.infn.mw.iam.api.scim.model; +import static it.infn.mw.iam.api.scim.model.ScimConstants.AARC_GROUP_SCHEMA; import static it.infn.mw.iam.api.scim.model.ScimConstants.INDIGO_GROUP_SCHEMA; import java.util.Collections; @@ -100,6 +101,7 @@ public Builder(String displayName) { super(); schemas.add(GROUP_SCHEMA); schemas.add(INDIGO_GROUP_SCHEMA); + schemas.add(AARC_GROUP_SCHEMA); this.displayName = displayName; indigoGroup = ScimIndigoGroup.getBuilder().build(); } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java index b18a4097a4..712559c022 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java @@ -15,6 +15,7 @@ */ package it.infn.mw.iam.api.scim.model; +import static it.infn.mw.iam.api.scim.model.ScimConstants.AARC_USER_SCHEMA; import static it.infn.mw.iam.api.scim.model.ScimConstants.INDIGO_USER_SCHEMA; import java.util.ArrayList; @@ -83,6 +84,9 @@ public interface UpdateUserValidation extends Default { @Valid private final ScimIndigoUser indigoUser; + @Valid + private final ScimAarcUser aarcUser; + @JsonCreator private ScimUser(@JsonProperty("id") String id, @JsonProperty("externalId") String externalId, @JsonProperty("meta") ScimMeta meta, @JsonProperty("schemas") Set schemas, @@ -98,7 +102,8 @@ private ScimUser(@JsonProperty("id") String id, @JsonProperty("externalId") Stri @JsonProperty("photos") List photos, @JsonProperty("groups") Set groups, @JsonProperty("x509Certificates") List x509Certificates, - @JsonProperty(INDIGO_USER_SCHEMA) ScimIndigoUser indigoUser) { + @JsonProperty(INDIGO_USER_SCHEMA) ScimIndigoUser indigoUser, + @JsonProperty(AARC_USER_SCHEMA) ScimAarcUser aarcUser) { super(id, externalId, meta, schemas); @@ -119,6 +124,7 @@ private ScimUser(@JsonProperty("id") String id, @JsonProperty("externalId") Stri this.groups = groups; this.addresses = addresses; this.indigoUser = indigoUser; + this.aarcUser = aarcUser; } private ScimUser(Builder b) { @@ -143,6 +149,11 @@ private ScimUser(Builder b) { } else { this.indigoUser = null; } + if (!b.aarcUserBuilder.equals(ScimAarcUser.builder())) { + this.aarcUser = b.aarcUserBuilder.build(); + } else { + this.aarcUser = null; + } this.groups = b.groups; this.password = b.password; } @@ -289,6 +300,12 @@ public boolean hasGroups() { return groups != null && !groups.isEmpty(); } + @JsonProperty(value = ScimConstants.AARC_USER_SCHEMA) + public ScimAarcUser getAarcUser() { + + return aarcUser; + } + public static Builder builder(String username) { return new Builder(username); @@ -319,11 +336,13 @@ public static class Builder extends ScimResource.Builder { private List addresses = new ArrayList<>(); private List photos = new ArrayList<>(); private ScimIndigoUser.Builder indigoUserBuilder = ScimIndigoUser.builder(); + private ScimAarcUser.Builder aarcUserBuilder = ScimAarcUser.builder(); public Builder() { super(); schemas.add(USER_SCHEMA); schemas.add(INDIGO_USER_SCHEMA); + schemas.add(AARC_USER_SCHEMA); } public Builder(String userName) { @@ -577,10 +596,19 @@ public Builder addManagedGroup(ScimGroupRef groupRef) { return this; } + public Builder voPersonId(String voPersonId) { + aarcUserBuilder.voPersonId(voPersonId); + return this; + } + + public Builder organizationName(String organizationName) { + aarcUserBuilder.organizationName(organizationName); + return this; + } + public ScimUser build() { return new ScimUser(this); } - } } diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/converter/UserConverterTest.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/converter/UserConverterTest.java index aa6daa12ba..727dca8785 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/converter/UserConverterTest.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/converter/UserConverterTest.java @@ -34,6 +34,7 @@ import it.infn.mw.iam.api.scim.converter.UserConverter; import it.infn.mw.iam.api.scim.converter.X509CertificateConverter; import it.infn.mw.iam.api.scim.model.ScimUser; +import it.infn.mw.iam.config.IamProperties; import it.infn.mw.iam.config.scim.ScimProperties; import it.infn.mw.iam.persistence.model.IamAccount; import it.infn.mw.iam.persistence.model.IamUserInfo; @@ -56,7 +57,9 @@ class UserConverterTest { @Mock private AccountGroupManagerService groupManagerService; @Mock - private ScimProperties properties; + private ScimProperties scimProperties; + @Mock + private IamProperties iamProperties; private UserConverter userConverter; @@ -64,9 +67,9 @@ class UserConverterTest { void setup() { lenient().when(resourceLocationProvider.userLocation(anyString())).thenReturn("User location"); - userConverter = - new UserConverter(properties, resourceLocationProvider, addressConverter, oidcIdConverter, - sshKeyConverter, samlIdConverter, x509CertificateIamConverter, groupManagerService); + userConverter = new UserConverter(scimProperties, resourceLocationProvider, addressConverter, + oidcIdConverter, sshKeyConverter, samlIdConverter, x509CertificateIamConverter, + groupManagerService, iamProperties); } @Test diff --git a/iam-persistence/src/main/java/it/infn/mw/iam/persistence/model/IamAccount.java b/iam-persistence/src/main/java/it/infn/mw/iam/persistence/model/IamAccount.java index 1b29a030c3..432d330679 100644 --- a/iam-persistence/src/main/java/it/infn/mw/iam/persistence/model/IamAccount.java +++ b/iam-persistence/src/main/java/it/infn/mw/iam/persistence/model/IamAccount.java @@ -365,8 +365,7 @@ public void unlinkSshKeys(Collection keys) { private void sanitizePrimarySshKey() { - if (!sshKeys.isEmpty() - && sshKeys.stream().filter(IamSshKey::isPrimary).findAny().isEmpty()) { + if (!sshKeys.isEmpty() && sshKeys.stream().filter(IamSshKey::isPrimary).findAny().isEmpty()) { sshKeys.iterator().next().setPrimary(true); } } From 8f8ad7580d124896dba636d3877d6af84e89f2e4 Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Thu, 16 Apr 2026 13:02:39 +0200 Subject: [PATCH 02/16] Add voPersonExternalAffiliation attribute and fix UserConverterTests --- .../mw/iam/api/scim/converter/UserConverter.java | 15 ++++++++++++--- .../infn/mw/iam/api/scim/model/ScimAarcUser.java | 14 +++++--------- .../it/infn/mw/iam/api/scim/model/ScimUser.java | 5 +++++ .../oauth/profile/aarc/AarcClaimValueHelper.java | 5 +++-- ...ConverterTest.java => UserConverterTests.java} | 8 ++++++-- 5 files changed, 31 insertions(+), 16 deletions(-) rename iam-login-service/src/test/java/it/infn/mw/iam/test/scim/converter/{UserConverterTest.java => UserConverterTests.java} (93%) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java index 5ebc1154c4..9e89f7d23a 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java @@ -23,6 +23,7 @@ import it.infn.mw.iam.api.account.group_manager.AccountGroupManagerService; import it.infn.mw.iam.api.scim.exception.ScimException; import it.infn.mw.iam.api.scim.model.ScimAddress; +import it.infn.mw.iam.api.scim.model.ScimAffiliation; import it.infn.mw.iam.api.scim.model.ScimAttribute; import it.infn.mw.iam.api.scim.model.ScimGroupRef; import it.infn.mw.iam.api.scim.model.ScimLabel; @@ -243,9 +244,6 @@ public ScimUser dtoFromEntity(IamAccount entity) { builder.affiliation(entity.getAffiliation()); } - builder.voPersonId(entity.getUuid() + "@" + iamProperties.getOrganisation().getName()); - builder.organizationName(iamProperties.getOrganisation().getName()); - for (LabelDescriptor ld : scimProperties.getIncludeLabels()) { entity.getLabelByPrefixAndName(ld.getPrefix(), ld.getName()) .ifPresent(el -> builder.addLabel(ScimLabel.builder() @@ -277,6 +275,17 @@ public ScimUser dtoFromEntity(IamAccount entity) { entity.getAuthorities().forEach(a -> builder.addAuthority(a.getAuthority())); } + builder.voPersonId(entity.getUuid() + "@" + iamProperties.getOrganisation().getName()); + builder.organizationName(iamProperties.getOrganisation().getName()); + + if (entity.hasAffiliation()) { + builder.addVoPersonExternalAffiliation(new ScimAffiliation( + entity.getAffiliation() + "@" + iamProperties.getOrganisation().getName())); + } else { + builder.addVoPersonExternalAffiliation( + new ScimAffiliation("member" + "@" + iamProperties.getOrganisation().getName())); + } + return builder.build(); } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java index bbe13f7cc8..6c5d33e3dc 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java @@ -18,14 +18,14 @@ import java.util.LinkedList; import java.util.List; -import javax.validation.Valid; -import javax.validation.constraints.NotNull; - import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonIgnoreProperties(value = {"voPersonId", "organizationName", "schacHomeOrganization", + "voPersonExternalAffiliation", "assurance"}, allowGetters = true) public class ScimAarcUser { public enum AARC_USER_SCHEMA { @@ -51,20 +51,16 @@ public String toString() { } } - @NotNull private final String voPersonId; private final String organizationName; private final String schacHomeOrganization; - @NotNull private final List voPersonExternalAffiliations; - @Valid private final List assurance; - @Valid private final List entitlements; @JsonCreator @@ -126,7 +122,7 @@ public static class Builder { private String organizationName; private String schacHomeOrganization; - private List voPersonExternalAffiliations; + private List voPersonExternalAffiliations = new LinkedList<>(); private List assurance = new LinkedList<>(); private List entitlements = new LinkedList<>(); @@ -145,7 +141,7 @@ public Builder schacHomeOrganization(String schacHomeOrganization) { return this; } - public Builder voPersonExternalAffiliation(ScimAffiliation affiliation) { + public Builder addVoPersonExternalAffiliation(ScimAffiliation affiliation) { this.voPersonExternalAffiliations.add(affiliation); return this; } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java index 712559c022..901f5eac49 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java @@ -606,6 +606,11 @@ public Builder organizationName(String organizationName) { return this; } + public Builder addVoPersonExternalAffiliation(ScimAffiliation value) { + aarcUserBuilder.addVoPersonExternalAffiliation(value); + return this; + } + public ScimUser build() { return new ScimUser(this); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/oauth/profile/aarc/AarcClaimValueHelper.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/oauth/profile/aarc/AarcClaimValueHelper.java index 19980dcbdb..66d08967bd 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/oauth/profile/aarc/AarcClaimValueHelper.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/oauth/profile/aarc/AarcClaimValueHelper.java @@ -39,7 +39,8 @@ @SuppressWarnings("deprecation") public class AarcClaimValueHelper extends IamClaimValueHelper { - public static final String AARC_VERSION_URI = "https://aarc-community.org/attribute/profile/version/1.0"; + public static final String AARC_VERSION_URI = + "https://aarc-community.org/attribute/profile/version/1.0"; public static final String REFEDS_ASSURANCE_URI = "https://refeds.org/assurance"; public static final String REFEDS_ASSURANCE_IAP_LOW_URI = "https://refeds.org/assurance/IAP/low"; @@ -106,7 +107,7 @@ public Object resolveClaim(String claimName, OAuth2Authentication auth, AuthenticationUtils.getExternalAuthenticationInfo(auth.getUserAuthentication()); if (userAuth.isPresent()) { Set scopedAffiliations = new HashSet<>(); - if (account.get().getUserInfo().getAffiliation() != null) { + if (account.get().getAffiliation() != null) { scopedAffiliations .add(format(SCOPED_FORMAT, account.get().getUserInfo().getAffiliation(), properties.getOrganisation().getName())); diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/converter/UserConverterTest.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/converter/UserConverterTests.java similarity index 93% rename from iam-login-service/src/test/java/it/infn/mw/iam/test/scim/converter/UserConverterTest.java rename to iam-login-service/src/test/java/it/infn/mw/iam/test/scim/converter/UserConverterTests.java index 727dca8785..eb8a851c6c 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/converter/UserConverterTest.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/converter/UserConverterTests.java @@ -35,12 +35,13 @@ import it.infn.mw.iam.api.scim.converter.X509CertificateConverter; import it.infn.mw.iam.api.scim.model.ScimUser; import it.infn.mw.iam.config.IamProperties; +import it.infn.mw.iam.config.IamProperties.Organisation; import it.infn.mw.iam.config.scim.ScimProperties; import it.infn.mw.iam.persistence.model.IamAccount; import it.infn.mw.iam.persistence.model.IamUserInfo; @ExtendWith(MockitoExtension.class) -class UserConverterTest { +class UserConverterTests { @Mock private ScimResourceLocationProvider resourceLocationProvider; @@ -60,13 +61,16 @@ class UserConverterTest { private ScimProperties scimProperties; @Mock private IamProperties iamProperties; + @Mock + private Organisation org; private UserConverter userConverter; @BeforeEach void setup() { - lenient().when(resourceLocationProvider.userLocation(anyString())).thenReturn("User location"); + lenient().when(iamProperties.getOrganisation()).thenReturn(org); + lenient().when(iamProperties.getOrganisation().getName()).thenReturn("indigo-dc"); userConverter = new UserConverter(scimProperties, resourceLocationProvider, addressConverter, oidcIdConverter, sshKeyConverter, samlIdConverter, x509CertificateIamConverter, groupManagerService, iamProperties); From f4fe8e4c2ab1f6d5cb82c8c470e82c6f18c2b3cf Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Fri, 17 Apr 2026 12:04:45 +0200 Subject: [PATCH 03/16] Add name and email attributes --- .../iam/api/scim/converter/UserConverter.java | 3 ++ .../mw/iam/api/scim/model/ScimAarcName.java | 40 +++++++++++++++++++ .../mw/iam/api/scim/model/ScimAarcUser.java | 36 +++++++++++++++-- .../infn/mw/iam/api/scim/model/ScimName.java | 2 +- .../infn/mw/iam/api/scim/model/ScimUser.java | 10 +++++ 5 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcName.java diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java index 9e89f7d23a..27a2e0f85d 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java @@ -22,6 +22,7 @@ import it.infn.mw.iam.api.account.group_manager.AccountGroupManagerService; import it.infn.mw.iam.api.scim.exception.ScimException; +import it.infn.mw.iam.api.scim.model.ScimAarcName; import it.infn.mw.iam.api.scim.model.ScimAddress; import it.infn.mw.iam.api.scim.model.ScimAffiliation; import it.infn.mw.iam.api.scim.model.ScimAttribute; @@ -277,6 +278,8 @@ public ScimUser dtoFromEntity(IamAccount entity) { builder.voPersonId(entity.getUuid() + "@" + iamProperties.getOrganisation().getName()); builder.organizationName(iamProperties.getOrganisation().getName()); + builder.addAarcName(new ScimAarcName(getScimName(entity))); + builder.addAarcEmail(entity.getUserInfo().getEmail()); if (entity.hasAffiliation()) { builder.addVoPersonExternalAffiliation(new ScimAffiliation( diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcName.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcName.java new file mode 100644 index 0000000000..5928d09532 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcName.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package it.infn.mw.iam.api.scim.model; + +public class ScimAarcName { + private final String displayName; + private final String givenName; + private final String familyName; + + public ScimAarcName(ScimName name) { + this.displayName = name.getFormatted(); + this.givenName = name.getGivenName(); + this.familyName = name.getFamilyName(); + } + + public String getDisplayName() { + return displayName; + } + + public String getGivenName() { + return givenName; + } + + public String getFamilyName() { + return familyName; + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java index 6c5d33e3dc..beccfc389c 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java @@ -24,13 +24,15 @@ import com.fasterxml.jackson.annotation.JsonProperty; @JsonInclude(JsonInclude.Include.NON_EMPTY) -@JsonIgnoreProperties(value = {"voPersonId", "organizationName", "schacHomeOrganization", - "voPersonExternalAffiliation", "assurance"}, allowGetters = true) +@JsonIgnoreProperties(value = {"voPersonId", "name", "email", "organizationName", + "schacHomeOrganization", "voPersonExternalAffiliation", "assurance"}, allowGetters = true) public class ScimAarcUser { public enum AARC_USER_SCHEMA { // @formatter:off + NAME(ScimConstants.AARC_USER_SCHEMA + ".name"), + EMAIL(ScimConstants.AARC_USER_SCHEMA + ".email"), VOPERSON_ID(ScimConstants.AARC_USER_SCHEMA + ".voPersonId"), ORGANIZATION_NAME(ScimConstants.AARC_USER_SCHEMA + ".organizationName"), SCHAC_HOME_ORGANIZATION(ScimConstants.AARC_USER_SCHEMA + ".schacHomeOrganization"), @@ -53,6 +55,10 @@ public String toString() { private final String voPersonId; + private final ScimAarcName name; + + private final String email; + private final String organizationName; private final String schacHomeOrganization; @@ -65,6 +71,7 @@ public String toString() { @JsonCreator private ScimAarcUser(@JsonProperty("voPersonId") String voPersonId, + @JsonProperty("name") ScimAarcName name, @JsonProperty("email") String email, @JsonProperty("organizationName") String organizationName, @JsonProperty("schacHomeOrganization") String schacHomeOrganization, @JsonProperty("voPersonExternalAffiliations") List voPersonExternalAffiliations, @@ -72,6 +79,8 @@ private ScimAarcUser(@JsonProperty("voPersonId") String voPersonId, @JsonProperty("entitlements") List entitlements) { this.voPersonId = voPersonId; + this.name = name; + this.email = email; this.organizationName = organizationName; this.schacHomeOrganization = schacHomeOrganization; this.voPersonExternalAffiliations = voPersonExternalAffiliations; @@ -81,6 +90,8 @@ private ScimAarcUser(@JsonProperty("voPersonId") String voPersonId, private ScimAarcUser(Builder b) { this.voPersonId = b.voPersonId; + this.name = b.name; + this.email = b.email; this.organizationName = b.organizationName; this.schacHomeOrganization = b.schacHomeOrganization; this.voPersonExternalAffiliations = b.voPersonExternalAffiliations; @@ -96,6 +107,14 @@ public String getOrganizationName() { return organizationName; } + public ScimAarcName getName() { + return name; + } + + public String getEmail() { + return email; + } + public String getSchacHomeOrganization() { return schacHomeOrganization; } @@ -119,7 +138,8 @@ public static Builder builder() { public static class Builder { private String voPersonId; - + private ScimAarcName name; + private String email; private String organizationName; private String schacHomeOrganization; private List voPersonExternalAffiliations = new LinkedList<>(); @@ -131,6 +151,16 @@ public Builder voPersonId(String voPersonId) { return this; } + public Builder name(ScimAarcName name) { + this.name = name; + return this; + } + + public Builder email(String email) { + this.email = email; + return this; + } + public Builder organizationName(String organizationName) { this.organizationName = organizationName; return this; diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimName.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimName.java index 1cf56ac33a..cdfbc0585f 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimName.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimName.java @@ -16,9 +16,9 @@ package it.infn.mw.iam.api.scim.model; import javax.annotation.Generated; +import javax.validation.constraints.NotBlank; import org.hibernate.validator.constraints.Length; -import javax.validation.constraints.NotBlank; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java index 901f5eac49..ad8ec0d9ec 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java @@ -611,6 +611,16 @@ public Builder addVoPersonExternalAffiliation(ScimAffiliation value) { return this; } + public Builder addAarcName(ScimAarcName name) { + aarcUserBuilder.name(name); + return this; + } + + public Builder addAarcEmail(String email) { + aarcUserBuilder.email(email); + return this; + } + public ScimUser build() { return new ScimUser(this); From 13595266a2336ffff413f009820665549bfa9966 Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Fri, 17 Apr 2026 14:11:56 +0200 Subject: [PATCH 04/16] Add assurance and entitlements attributes --- .../iam/api/scim/converter/UserConverter.java | 41 +++++++++++++++++++ .../infn/mw/iam/api/scim/model/ScimUser.java | 10 +++++ 2 files changed, 51 insertions(+) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java index 27a2e0f85d..baa25cdd5f 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java @@ -18,6 +18,9 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import java.util.HashSet; +import java.util.Set; + import org.springframework.stereotype.Service; import it.infn.mw.iam.api.account.group_manager.AccountGroupManagerService; @@ -25,7 +28,9 @@ import it.infn.mw.iam.api.scim.model.ScimAarcName; import it.infn.mw.iam.api.scim.model.ScimAddress; import it.infn.mw.iam.api.scim.model.ScimAffiliation; +import it.infn.mw.iam.api.scim.model.ScimAssurance; import it.infn.mw.iam.api.scim.model.ScimAttribute; +import it.infn.mw.iam.api.scim.model.ScimEntitlement; import it.infn.mw.iam.api.scim.model.ScimGroupRef; import it.infn.mw.iam.api.scim.model.ScimLabel; import it.infn.mw.iam.api.scim.model.ScimMeta; @@ -49,6 +54,12 @@ @Service public class UserConverter implements Converter { + public static final String REFEDS_ASSURANCE_URI = "https://refeds.org/assurance"; + public static final String REFEDS_ASSURANCE_IAP_LOW_URI = "https://refeds.org/assurance/IAP/low"; + + public static final Set DEFAULT_LOA = + Set.of(REFEDS_ASSURANCE_URI, REFEDS_ASSURANCE_IAP_LOW_URI); + private final ScimResourceLocationProvider resourceLocationProvider; private final AddressConverter addressConverter; @@ -289,6 +300,11 @@ public ScimUser dtoFromEntity(IamAccount entity) { new ScimAffiliation("member" + "@" + iamProperties.getOrganisation().getName())); } + DEFAULT_LOA.forEach(a -> builder.addAssurance(new ScimAssurance(a))); + + resolveGroups(entity.getUserInfo()) + .forEach(e -> builder.addEntitlements(new ScimEntitlement(e))); + return builder.build(); } @@ -338,4 +354,29 @@ private ScimPhoto getScimPhoto(IamAccount entity) { return ScimPhoto.builder().value(entity.getUserInfo().getPicture()).build(); } + + public Set resolveGroups(IamUserInfo userInfo) { + + Set encodedGroups = new HashSet<>(); + userInfo.getGroups().forEach(g -> encodedGroups.add(encodeGroup(g))); + return encodedGroups; + } + + private String encodeGroup(IamGroup group) { + + var aarcConfig = iamProperties.getAarcProfile(); + + String urnNid = aarcConfig.getUrnNid(); + String urnDelegatedNamespace = aarcConfig.getUrnDelegatedNamespace(); + String encodedGroupName = group.getName().replace("/", ":"); + + String encodedSubnamespace = ""; + String urnSubnamespaces = aarcConfig.getUrnSubnamespaces(); + if (urnSubnamespaces != null && !urnSubnamespaces.isBlank()) { + encodedSubnamespace = ":" + String.join(":", urnSubnamespaces.trim().split("\\s+")); + } + + return String.format("urn:%s:%s%s:group:%s", urnNid, urnDelegatedNamespace, encodedSubnamespace, + encodedGroupName); + } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java index ad8ec0d9ec..bc9c747ab1 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java @@ -621,6 +621,16 @@ public Builder addAarcEmail(String email) { return this; } + public Builder addAssurance(ScimAssurance assurance) { + aarcUserBuilder.addAssurance(assurance); + return this; + } + + public Builder addEntitlements(ScimEntitlement entitlement) { + aarcUserBuilder.addEntitlement(entitlement); + return this; + } + public ScimUser build() { return new ScimUser(this); From 0fd007ed42b0291ba09a18fd41d8abd82cc9dcd9 Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Fri, 17 Apr 2026 17:06:30 +0200 Subject: [PATCH 05/16] Fix UserConverterTests by adding a group to the account --- .../infn/mw/iam/test/scim/converter/UserConverterTests.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/converter/UserConverterTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/converter/UserConverterTests.java index eb8a851c6c..1cb35872c7 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/converter/UserConverterTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/converter/UserConverterTests.java @@ -19,6 +19,8 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.lenient; +import java.util.Set; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -86,6 +88,9 @@ void testEntityWithAffiliationProduceDtoWithAffiliation() { iamAccount.setUsername("Test User"); iamAccount.setUuid("UUID"); iamAccount.setUserInfo(userInfo); + iamAccount.setGroups(Set.of()); + + userInfo.setIamAccount(iamAccount); ScimUser scimUser = userConverter.dtoFromEntity(iamAccount); assertEquals("Test user affiliation", scimUser.getIndigoUser().getAffiliation()); From cabb5f4f3f9a53d2d1fb401d5208e84bbe9bfdec Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Mon, 20 Apr 2026 12:18:33 +0200 Subject: [PATCH 06/16] Add property to enable aarc scim extension default set to false --- .../iam/api/scim/converter/UserConverter.java | 36 ++++++++++--------- .../infn/mw/iam/api/scim/model/ScimUser.java | 14 ++++++-- .../mw/iam/config/scim/ScimProperties.java | 9 +++++ .../src/main/resources/application.yml | 1 + 4 files changed, 42 insertions(+), 18 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java index baa25cdd5f..ddbade4ec2 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java @@ -287,24 +287,28 @@ public ScimUser dtoFromEntity(IamAccount entity) { entity.getAuthorities().forEach(a -> builder.addAuthority(a.getAuthority())); } - builder.voPersonId(entity.getUuid() + "@" + iamProperties.getOrganisation().getName()); - builder.organizationName(iamProperties.getOrganisation().getName()); - builder.addAarcName(new ScimAarcName(getScimName(entity))); - builder.addAarcEmail(entity.getUserInfo().getEmail()); - - if (entity.hasAffiliation()) { - builder.addVoPersonExternalAffiliation(new ScimAffiliation( - entity.getAffiliation() + "@" + iamProperties.getOrganisation().getName())); - } else { - builder.addVoPersonExternalAffiliation( - new ScimAffiliation("member" + "@" + iamProperties.getOrganisation().getName())); + builder.enableAarc(scimProperties.isEnableAarc()); + + if (scimProperties.isEnableAarc()) { + builder.voPersonId(entity.getUuid() + "@" + iamProperties.getOrganisation().getName()); + builder.organizationName(iamProperties.getOrganisation().getName()); + builder.addAarcName(new ScimAarcName(getScimName(entity))); + builder.addAarcEmail(entity.getUserInfo().getEmail()); + + if (entity.hasAffiliation()) { + builder.addVoPersonExternalAffiliation(new ScimAffiliation( + entity.getAffiliation() + "@" + iamProperties.getOrganisation().getName())); + } else { + builder.addVoPersonExternalAffiliation( + new ScimAffiliation("member" + "@" + iamProperties.getOrganisation().getName())); + } + + DEFAULT_LOA.forEach(a -> builder.addAssurance(new ScimAssurance(a))); + + resolveGroups(entity.getUserInfo()) + .forEach(e -> builder.addEntitlements(new ScimEntitlement(e))); } - DEFAULT_LOA.forEach(a -> builder.addAssurance(new ScimAssurance(a))); - - resolveGroups(entity.getUserInfo()) - .forEach(e -> builder.addEntitlements(new ScimEntitlement(e))); - return builder.build(); } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java index bc9c747ab1..5d4e77baf7 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java @@ -149,7 +149,7 @@ private ScimUser(Builder b) { } else { this.indigoUser = null; } - if (!b.aarcUserBuilder.equals(ScimAarcUser.builder())) { + if (b.enableAarc && !b.aarcUserBuilder.equals(ScimAarcUser.builder())) { this.aarcUser = b.aarcUserBuilder.build(); } else { this.aarcUser = null; @@ -337,12 +337,22 @@ public static class Builder extends ScimResource.Builder { private List photos = new ArrayList<>(); private ScimIndigoUser.Builder indigoUserBuilder = ScimIndigoUser.builder(); private ScimAarcUser.Builder aarcUserBuilder = ScimAarcUser.builder(); + private boolean enableAarc = false; public Builder() { super(); schemas.add(USER_SCHEMA); schemas.add(INDIGO_USER_SCHEMA); - schemas.add(AARC_USER_SCHEMA); + } + + public Builder enableAarc(boolean enableAarc) { + this.enableAarc = enableAarc; + if (enableAarc) { + schemas.add(AARC_USER_SCHEMA); + } else { + schemas.remove(AARC_USER_SCHEMA); + } + return this; } public Builder(String userName) { diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/scim/ScimProperties.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/scim/ScimProperties.java index 8f64ac280b..bebb04bd98 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/scim/ScimProperties.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/scim/ScimProperties.java @@ -63,6 +63,7 @@ public void setName(String name) { List includeAttributes = Lists.newArrayList(); boolean includeAuthorities = false; boolean includeManagedGroups = false; + boolean enableAarc = false; public List getIncludeLabels() { return includeLabels; @@ -95,4 +96,12 @@ public boolean isIncludeManagedGroups() { public void setIncludeManagedGroups(boolean includeManagedGroups) { this.includeManagedGroups = includeManagedGroups; } + + public boolean isEnableAarc() { + return enableAarc; + } + + public void setEnableAarc(boolean enableAarc) { + this.enableAarc = enableAarc; + } } diff --git a/iam-login-service/src/main/resources/application.yml b/iam-login-service/src/main/resources/application.yml index 924ae10ff3..bfe7eadb37 100644 --- a/iam-login-service/src/main/resources/application.yml +++ b/iam-login-service/src/main/resources/application.yml @@ -291,3 +291,4 @@ lifecycle: scim: include_authorities: ${IAM_SCIM_INCLUDE_AUTHORITIES:false} include_managed_groups: ${IAM_SCIM_INCLUDE_MANAGED_GROUPS:false} + enable-aarc: ${IAM_SCIM_ENABLE_AARC:false} From a7d98f771057d9b13df5270d90f4545c62dadfd5 Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Mon, 20 Apr 2026 12:58:22 +0200 Subject: [PATCH 07/16] Add test --- .../test/scim/user/ScimAarcSchemaTests.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 iam-login-service/src/test/java/it/infn/mw/iam/test/scim/user/ScimAarcSchemaTests.java diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/user/ScimAarcSchemaTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/user/ScimAarcSchemaTests.java new file mode 100644 index 0000000000..314638d082 --- /dev/null +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/user/ScimAarcSchemaTests.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package it.infn.mw.iam.test.scim.user; + +import static org.hamcrest.CoreMatchers.hasItems; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.TestPropertySource; + +import it.infn.mw.iam.IamLoginService; +import it.infn.mw.iam.api.scim.model.ScimConstants; +import it.infn.mw.iam.api.scim.model.ScimUser; +import it.infn.mw.iam.test.scim.ScimRestUtilsMvc; + +@SpringBootTest(classes = {IamLoginService.class, ScimRestUtilsMvc.class}, + webEnvironment = WebEnvironment.MOCK) +@AutoConfigureMockMvc +@TestPropertySource(properties = "scim.enable-aarc=true") +class ScimAarcSchemaTests { + + @Autowired + private ScimRestUtilsMvc scimUtils; + + @Test + @WithMockUser(username = "admin", roles = "ADMIN") + void testScimAarcUser() throws Exception { + + scimUtils.getUsers() + .andExpect(jsonPath("$.Resources[0].schemas", hasItems(ScimUser.USER_SCHEMA, + ScimConstants.INDIGO_USER_SCHEMA, ScimConstants.AARC_USER_SCHEMA))); + } +} From fd8ad6e007554eaf760652890ee12898487ffd12 Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Tue, 12 May 2026 19:02:50 +0200 Subject: [PATCH 08/16] Remove AARC Group schema extension --- .../main/java/it/infn/mw/iam/api/scim/model/ScimConstants.java | 1 - .../src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java | 2 -- 2 files changed, 3 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimConstants.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimConstants.java index 53d79c45a3..3c4cefd40e 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimConstants.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimConstants.java @@ -21,5 +21,4 @@ public interface ScimConstants { final String INDIGO_USER_SCHEMA = "urn:indigo-dc:scim:schemas:IndigoUser"; final String INDIGO_GROUP_SCHEMA = "urn:indigo-dc:scim:schemas:IndigoGroup"; final String AARC_USER_SCHEMA = "urn:geant:aarc-community:scim:schemas:core:1.0:User"; - final String AARC_GROUP_SCHEMA = "urn:geant:aarc-community:scim:schemas:core:1.0:Group"; } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java index e167b6d41b..60cb02cd13 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java @@ -15,7 +15,6 @@ */ package it.infn.mw.iam.api.scim.model; -import static it.infn.mw.iam.api.scim.model.ScimConstants.AARC_GROUP_SCHEMA; import static it.infn.mw.iam.api.scim.model.ScimConstants.INDIGO_GROUP_SCHEMA; import java.util.Collections; @@ -101,7 +100,6 @@ public Builder(String displayName) { super(); schemas.add(GROUP_SCHEMA); schemas.add(INDIGO_GROUP_SCHEMA); - schemas.add(AARC_GROUP_SCHEMA); this.displayName = displayName; indigoGroup = ScimIndigoGroup.getBuilder().build(); } From f3b7550c04e08df4ede0fa0b4bc6b6e513d4b33d Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Wed, 13 May 2026 14:46:15 +0200 Subject: [PATCH 09/16] Restore small changes --- .../mw/iam/core/oauth/profile/aarc/AarcClaimValueHelper.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/oauth/profile/aarc/AarcClaimValueHelper.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/oauth/profile/aarc/AarcClaimValueHelper.java index 66d08967bd..19980dcbdb 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/oauth/profile/aarc/AarcClaimValueHelper.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/oauth/profile/aarc/AarcClaimValueHelper.java @@ -39,8 +39,7 @@ @SuppressWarnings("deprecation") public class AarcClaimValueHelper extends IamClaimValueHelper { - public static final String AARC_VERSION_URI = - "https://aarc-community.org/attribute/profile/version/1.0"; + public static final String AARC_VERSION_URI = "https://aarc-community.org/attribute/profile/version/1.0"; public static final String REFEDS_ASSURANCE_URI = "https://refeds.org/assurance"; public static final String REFEDS_ASSURANCE_IAP_LOW_URI = "https://refeds.org/assurance/IAP/low"; @@ -107,7 +106,7 @@ public Object resolveClaim(String claimName, OAuth2Authentication auth, AuthenticationUtils.getExternalAuthenticationInfo(auth.getUserAuthentication()); if (userAuth.isPresent()) { Set scopedAffiliations = new HashSet<>(); - if (account.get().getAffiliation() != null) { + if (account.get().getUserInfo().getAffiliation() != null) { scopedAffiliations .add(format(SCOPED_FORMAT, account.get().getUserInfo().getAffiliation(), properties.getOrganisation().getName())); From 4ba739af25bba0da0f81c61ecb5c10165296bf12 Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Thu, 14 May 2026 16:15:08 +0200 Subject: [PATCH 10/16] Remove enum --- .../mw/iam/api/scim/model/ScimAarcUser.java | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java index beccfc389c..d9efc76004 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java @@ -28,45 +28,13 @@ "schacHomeOrganization", "voPersonExternalAffiliation", "assurance"}, allowGetters = true) public class ScimAarcUser { - public enum AARC_USER_SCHEMA { - - // @formatter:off - NAME(ScimConstants.AARC_USER_SCHEMA + ".name"), - EMAIL(ScimConstants.AARC_USER_SCHEMA + ".email"), - VOPERSON_ID(ScimConstants.AARC_USER_SCHEMA + ".voPersonId"), - ORGANIZATION_NAME(ScimConstants.AARC_USER_SCHEMA + ".organizationName"), - SCHAC_HOME_ORGANIZATION(ScimConstants.AARC_USER_SCHEMA + ".schacHomeOrganization"), - VO_PERSON_EXTERNAL_AFFILIATIONS(ScimConstants.AARC_USER_SCHEMA + ".voPersonExternalAffiliations"), - ASSURANCE(ScimConstants.AARC_USER_SCHEMA + ".assurance"), - ENTITLEMENTS(ScimConstants.AARC_USER_SCHEMA + ".entitlements"); - // @formatter:on - - private final String text; - - AARC_USER_SCHEMA(String text) { - this.text = text; - } - - @Override - public String toString() { - return text; - } - } - private final String voPersonId; - private final ScimAarcName name; - private final String email; - private final String organizationName; - private final String schacHomeOrganization; - private final List voPersonExternalAffiliations; - private final List assurance; - private final List entitlements; @JsonCreator From dbac4a045a38b7931fb0cecc80b6664804a03c48 Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Thu, 14 May 2026 16:15:38 +0200 Subject: [PATCH 11/16] Improve test --- .../test/scim/user/ScimAarcSchemaTests.java | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/user/ScimAarcSchemaTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/user/ScimAarcSchemaTests.java index 314638d082..37a4d568c6 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/user/ScimAarcSchemaTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/user/ScimAarcSchemaTests.java @@ -15,6 +15,7 @@ */ package it.infn.mw.iam.test.scim.user; +import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.hasItems; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -29,6 +30,8 @@ import it.infn.mw.iam.IamLoginService; import it.infn.mw.iam.api.scim.model.ScimConstants; import it.infn.mw.iam.api.scim.model.ScimUser; +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.repository.IamAccountRepository; import it.infn.mw.iam.test.scim.ScimRestUtilsMvc; @SpringBootTest(classes = {IamLoginService.class, ScimRestUtilsMvc.class}, @@ -37,15 +40,42 @@ @TestPropertySource(properties = "scim.enable-aarc=true") class ScimAarcSchemaTests { + private static final String ACCOUNT_UUID = "73f16d93-2441-4a50-88ff-85360d78c6b5"; + public static final String REFEDS_ASSURANCE_URI = "https://refeds.org/assurance"; + public static final String REFEDS_ASSURANCE_IAP_LOW_URI = "https://refeds.org/assurance/IAP/low"; + @Autowired private ScimRestUtilsMvc scimUtils; + @Autowired + private IamAccountRepository accountRepo; + @Test @WithMockUser(username = "admin", roles = "ADMIN") - void testScimAarcUser() throws Exception { + void testScimAarcUserSchema() throws Exception { + + IamAccount account = accountRepo.findByUuid(ACCOUNT_UUID).orElseThrow(); scimUtils.getUsers() - .andExpect(jsonPath("$.Resources[0].schemas", hasItems(ScimUser.USER_SCHEMA, - ScimConstants.INDIGO_USER_SCHEMA, ScimConstants.AARC_USER_SCHEMA))); + .andExpect(jsonPath("$.Resources[0].schemas", + hasItems(ScimUser.USER_SCHEMA, ScimConstants.INDIGO_USER_SCHEMA, + ScimConstants.AARC_USER_SCHEMA))) + .andExpect(jsonPath("$.Resources[0]['" + ScimConstants.AARC_USER_SCHEMA + "'].voPersonId", + equalTo(ACCOUNT_UUID + "@indigo-dc"))) + .andExpect( + jsonPath("$.Resources[0]['" + ScimConstants.AARC_USER_SCHEMA + "'].name.displayName", + equalTo(account.getUserInfo().getName()))) + .andExpect( + jsonPath("$.Resources[0]['" + ScimConstants.AARC_USER_SCHEMA + "'].name.familyName", + equalTo(account.getUserInfo().getFamilyName()))) + .andExpect(jsonPath("$.Resources[0]['" + ScimConstants.AARC_USER_SCHEMA + "'].name.givenName", + equalTo(account.getUserInfo().getGivenName()))) + .andExpect(jsonPath("$.Resources[0]['" + ScimConstants.AARC_USER_SCHEMA + "'].email", + equalTo(account.getUserInfo().getEmail()))) + .andExpect(jsonPath("$.Resources[0]['" + ScimConstants.AARC_USER_SCHEMA + + "'].voPersonExternalAffiliations[*].value", hasItems("member@indigo-dc"))) + .andExpect( + jsonPath("$.Resources[0]['" + ScimConstants.AARC_USER_SCHEMA + "'].assurance[*].value", + hasItems(REFEDS_ASSURANCE_URI, REFEDS_ASSURANCE_IAP_LOW_URI))); } } From 42855b5279340434902d31bb18eff9b81691df8f Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Mon, 18 May 2026 12:02:44 +0200 Subject: [PATCH 12/16] Add scim aarc Group schema --- .../api/scim/converter/GroupConverter.java | 22 +++++-- .../mw/iam/api/scim/model/ScimAarcGroup.java | 58 +++++++++++++++++++ .../mw/iam/api/scim/model/ScimConstants.java | 1 + .../infn/mw/iam/api/scim/model/ScimGroup.java | 30 +++++++++- 4 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcGroup.java diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/GroupConverter.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/GroupConverter.java index 852d2d5b7b..adbb05644f 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/GroupConverter.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/GroupConverter.java @@ -23,11 +23,14 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; +import it.infn.mw.iam.api.scim.model.ScimAarcGroup; import it.infn.mw.iam.api.scim.model.ScimGroup; +import it.infn.mw.iam.api.scim.model.ScimGroup.Builder; import it.infn.mw.iam.api.scim.model.ScimGroupRef; import it.infn.mw.iam.api.scim.model.ScimIndigoGroup; import it.infn.mw.iam.api.scim.model.ScimLabel; import it.infn.mw.iam.api.scim.model.ScimMeta; +import it.infn.mw.iam.config.scim.ScimProperties; import it.infn.mw.iam.persistence.model.IamGroup; import it.infn.mw.iam.persistence.model.IamLabel; @@ -35,10 +38,11 @@ public class GroupConverter implements Converter { private final ScimResourceLocationProvider resourceLocationProvider; + private final ScimProperties scimProperties; - public GroupConverter(ScimResourceLocationProvider rlp) { - + public GroupConverter(ScimResourceLocationProvider rlp, ScimProperties scimProperties) { this.resourceLocationProvider = rlp; + this.scimProperties = scimProperties; } /** @@ -103,11 +107,19 @@ public ScimGroup dtoFromEntity(IamGroup entity) { scimIndigoGroup.labels(labels); } - return ScimGroup.builder(entity.getName()) + Builder builder = ScimGroup.builder(entity.getName()) .id(entity.getUuid()) .meta(meta) - .indigoGroup(scimIndigoGroup.build()) - .build(); + .indigoGroup(scimIndigoGroup.build()); + + builder.enableAarc(scimProperties.isEnableAarc()); + + if (scimProperties.isEnableAarc()) { + ScimAarcGroup aarcGroup = ScimAarcGroup.builder().members(members).build(); + builder.aarcGroup(aarcGroup); + } + + return builder.build(); } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcGroup.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcGroup.java new file mode 100644 index 0000000000..7bae0818d9 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcGroup.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package it.infn.mw.iam.api.scim.model; + +import java.util.Collections; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ScimAarcGroup { + + private final Set members; + + @JsonCreator + private ScimAarcGroup(@JsonProperty("members") Set members) { + this.members = members != null ? members : Collections.emptySet(); + } + + private ScimAarcGroup(Builder b) { + this.members = b.members != null ? b.members : Collections.emptySet(); + } + + public Set getMembers() { + return members; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private Set members = Collections.emptySet(); + + public Builder members(Set members) { + this.members = members; + return this; + } + + public ScimAarcGroup build() { + return new ScimAarcGroup(this); + } + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimConstants.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimConstants.java index 3c4cefd40e..53d79c45a3 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimConstants.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimConstants.java @@ -21,4 +21,5 @@ public interface ScimConstants { final String INDIGO_USER_SCHEMA = "urn:indigo-dc:scim:schemas:IndigoUser"; final String INDIGO_GROUP_SCHEMA = "urn:indigo-dc:scim:schemas:IndigoGroup"; final String AARC_USER_SCHEMA = "urn:geant:aarc-community:scim:schemas:core:1.0:User"; + final String AARC_GROUP_SCHEMA = "urn:geant:aarc-community:scim:schemas:core:1.0:Group"; } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java index 60cb02cd13..473745da29 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java @@ -15,6 +15,7 @@ */ package it.infn.mw.iam.api.scim.model; +import static it.infn.mw.iam.api.scim.model.ScimConstants.AARC_GROUP_SCHEMA; import static it.infn.mw.iam.api.scim.model.ScimConstants.INDIGO_GROUP_SCHEMA; import java.util.Collections; @@ -50,17 +51,23 @@ public final class ScimGroup extends ScimResource { @JsonProperty(value = ScimConstants.INDIGO_GROUP_SCHEMA) private final ScimIndigoGroup indigoGroup; + @JsonProperty(value = ScimConstants.AARC_GROUP_SCHEMA) + private final ScimAarcGroup aarcGroup; + + @JsonCreator private ScimGroup(@JsonProperty("id") String id, @JsonProperty("externalId") String externalId, @JsonProperty("meta") ScimMeta meta, @JsonProperty("schemas") Set schemas, @JsonProperty("displayName") String displayName, @JsonProperty("members") Set members, - @JsonProperty(INDIGO_GROUP_SCHEMA) ScimIndigoGroup indigoGroup) { + @JsonProperty(INDIGO_GROUP_SCHEMA) ScimIndigoGroup indigoGroup, + @JsonProperty(AARC_GROUP_SCHEMA) ScimAarcGroup aarcGroup) { super(id, externalId, meta, schemas); this.displayName = displayName; this.members = (members != null ? members : Collections.emptySet()); this.indigoGroup = (indigoGroup != null ? indigoGroup : ScimIndigoGroup.getBuilder().build()); + this.aarcGroup = aarcGroup; } private ScimGroup(Builder b) { @@ -69,6 +76,7 @@ private ScimGroup(Builder b) { this.displayName = b.displayName; this.members = b.members; this.indigoGroup = b.indigoGroup; + this.aarcGroup = b.aarcGroup; } public String getDisplayName() { @@ -95,6 +103,8 @@ public static class Builder extends ScimResource.Builder { private String displayName; private Set members = new HashSet<>(); private ScimIndigoGroup indigoGroup = null; + private ScimAarcGroup aarcGroup = null; + private boolean enableAarc = false; public Builder(String displayName) { super(); @@ -104,6 +114,18 @@ public Builder(String displayName) { indigoGroup = ScimIndigoGroup.getBuilder().build(); } + public Builder enableAarc(boolean enableAarc) { + + this.enableAarc = enableAarc; + + if (enableAarc) { + schemas.add(AARC_GROUP_SCHEMA); + } else { + schemas.remove(AARC_GROUP_SCHEMA); + } + return this; + } + public Builder id(String id) { this.id = id; @@ -127,6 +149,12 @@ public Builder indigoGroup(ScimIndigoGroup indigoGroup) { return this; } + public Builder aarcGroup(ScimAarcGroup aarcGroup) { + + this.aarcGroup = aarcGroup; + return this; + } + public ScimGroup build() { return new ScimGroup(this); From 922f8039ea532066e023c131dc66a239d40b89e3 Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Tue, 19 May 2026 12:16:21 +0200 Subject: [PATCH 13/16] Show members attribute following the core schema (containing display, value and $ref) --- .../api/scim/converter/GroupConverter.java | 29 +++++++++++++++++-- .../infn/mw/iam/api/scim/model/ScimGroup.java | 3 -- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/GroupConverter.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/GroupConverter.java index adbb05644f..df52d39004 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/GroupConverter.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/GroupConverter.java @@ -17,32 +17,45 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; +import it.infn.mw.iam.api.common.OffsetPageable; import it.infn.mw.iam.api.scim.model.ScimAarcGroup; import it.infn.mw.iam.api.scim.model.ScimGroup; import it.infn.mw.iam.api.scim.model.ScimGroup.Builder; import it.infn.mw.iam.api.scim.model.ScimGroupRef; import it.infn.mw.iam.api.scim.model.ScimIndigoGroup; import it.infn.mw.iam.api.scim.model.ScimLabel; +import it.infn.mw.iam.api.scim.model.ScimMemberRef; import it.infn.mw.iam.api.scim.model.ScimMeta; import it.infn.mw.iam.config.scim.ScimProperties; +import it.infn.mw.iam.core.user.IamAccountService; +import it.infn.mw.iam.persistence.model.IamAccount; import it.infn.mw.iam.persistence.model.IamGroup; import it.infn.mw.iam.persistence.model.IamLabel; +import it.infn.mw.iam.persistence.repository.IamAccountRepository; @Service public class GroupConverter implements Converter { private final ScimResourceLocationProvider resourceLocationProvider; private final ScimProperties scimProperties; + private final IamAccountRepository accountRepo; + private final IamAccountService accountService; - public GroupConverter(ScimResourceLocationProvider rlp, ScimProperties scimProperties) { + + public GroupConverter(ScimResourceLocationProvider rlp, ScimProperties scimProperties, + IamAccountRepository accountRepo, IamAccountService accountService) { this.resourceLocationProvider = rlp; this.scimProperties = scimProperties; + this.accountRepo = accountRepo; + this.accountService = accountService; } /** @@ -115,11 +128,23 @@ public ScimGroup dtoFromEntity(IamGroup entity) { builder.enableAarc(scimProperties.isEnableAarc()); if (scimProperties.isEnableAarc()) { + long totalUsers = accountRepo.count(); + OffsetPageable pr = new OffsetPageable(0, (int) totalUsers); + Page accounts = accountService.findGroupMembers(entity, pr); + Set members = new HashSet<>(); + + for (IamAccount a : accounts.getContent()) { + members.add(ScimMemberRef.builder() + .value(a.getUuid()) + .display(a.getUserInfo().getName()) + .ref(resourceLocationProvider.userLocation(a.getUuid())) + .build()); + } + ScimAarcGroup aarcGroup = ScimAarcGroup.builder().members(members).build(); builder.aarcGroup(aarcGroup); } return builder.build(); } - } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java index 473745da29..3e029941cd 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java @@ -104,7 +104,6 @@ public static class Builder extends ScimResource.Builder { private Set members = new HashSet<>(); private ScimIndigoGroup indigoGroup = null; private ScimAarcGroup aarcGroup = null; - private boolean enableAarc = false; public Builder(String displayName) { super(); @@ -116,8 +115,6 @@ public Builder(String displayName) { public Builder enableAarc(boolean enableAarc) { - this.enableAarc = enableAarc; - if (enableAarc) { schemas.add(AARC_GROUP_SCHEMA); } else { From b3febde44e533b3b81acd9bae5f286a62375c596 Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Tue, 19 May 2026 12:18:22 +0200 Subject: [PATCH 14/16] Add test enabling scim aarc schema --- .../mw/iam/test/scim/ScimRestUtilsMvc.java | 6 ++ .../test/scim/group/ScimAarcGroupTests.java | 57 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 iam-login-service/src/test/java/it/infn/mw/iam/test/scim/group/ScimAarcGroupTests.java diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/ScimRestUtilsMvc.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/ScimRestUtilsMvc.java index 1de8d8c251..e05c8dc707 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/ScimRestUtilsMvc.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/ScimRestUtilsMvc.java @@ -16,6 +16,7 @@ package it.infn.mw.iam.test.scim; import static it.infn.mw.iam.test.scim.ScimUtils.SCIM_CONTENT_TYPE; +import static it.infn.mw.iam.test.scim.ScimUtils.getGroupsLocation; import static it.infn.mw.iam.test.scim.ScimUtils.getMeLocation; import static it.infn.mw.iam.test.scim.ScimUtils.getUserLocation; import static it.infn.mw.iam.test.scim.ScimUtils.getUsersBulkLocation; @@ -117,6 +118,11 @@ public ResultActions getMe(HttpStatus expectedStatus) throws Exception { return doGet(getMeLocation(), SCIM_CONTENT_TYPE, expectedStatus); } + public ResultActions getGroups() throws Exception { + + return doGet(getGroupsLocation(), SCIM_CONTENT_TYPE, OK); + } + public ResultActions putUser(String uuid, ScimUser user, HttpStatus expectedStatus) throws Exception { diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/group/ScimAarcGroupTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/group/ScimAarcGroupTests.java new file mode 100644 index 0000000000..557ec4455a --- /dev/null +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/group/ScimAarcGroupTests.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package it.infn.mw.iam.test.scim.group; + +import static org.hamcrest.CoreMatchers.everyItem; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.hasKey; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.TestPropertySource; + +import it.infn.mw.iam.IamLoginService; +import it.infn.mw.iam.api.scim.model.ScimConstants; +import it.infn.mw.iam.api.scim.model.ScimGroup; +import it.infn.mw.iam.test.scim.ScimRestUtilsMvc; + +@SpringBootTest(classes = {IamLoginService.class, ScimRestUtilsMvc.class}, + webEnvironment = WebEnvironment.MOCK) +@AutoConfigureMockMvc +@TestPropertySource(properties = "scim.enable-aarc=true") +public class ScimAarcGroupTests { + + @Autowired + private ScimRestUtilsMvc scimUtils; + + @Test + @WithMockUser(username = "admin", roles = "ADMIN") + void testScimAarcGroupSchema() throws Exception { + + scimUtils.getGroups() + .andExpect(jsonPath("$.Resources[0].schemas", + hasItems(ScimGroup.GROUP_SCHEMA, ScimConstants.INDIGO_GROUP_SCHEMA, + ScimConstants.AARC_GROUP_SCHEMA))) + .andExpect(jsonPath("$.Resources[0]['" + ScimConstants.AARC_GROUP_SCHEMA + "'].members[*]", + everyItem(allOf(hasKey("value"), hasKey("$ref"), hasKey("display"))))); + } +} From 73179403fce01782bda37276df353f8e95a2572a Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Tue, 19 May 2026 12:48:46 +0200 Subject: [PATCH 15/16] Remove public modifier --- .../java/it/infn/mw/iam/test/scim/group/ScimAarcGroupTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/group/ScimAarcGroupTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/group/ScimAarcGroupTests.java index 557ec4455a..5c06bbc896 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/group/ScimAarcGroupTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/group/ScimAarcGroupTests.java @@ -38,7 +38,7 @@ webEnvironment = WebEnvironment.MOCK) @AutoConfigureMockMvc @TestPropertySource(properties = "scim.enable-aarc=true") -public class ScimAarcGroupTests { +class ScimAarcGroupTests { @Autowired private ScimRestUtilsMvc scimUtils; From 436801353baf0ecefad47edaad18a845b3c3109f Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Wed, 17 Jun 2026 16:49:41 +0200 Subject: [PATCH 16/16] Move displayName out of the name attribute --- .../iam/api/scim/converter/UserConverter.java | 3 ++- .../mw/iam/api/scim/model/ScimAarcName.java | 6 ------ .../mw/iam/api/scim/model/ScimAarcUser.java | 18 ++++++++++++++++-- .../infn/mw/iam/api/scim/model/ScimUser.java | 15 ++++++++++----- .../test/scim/user/ScimAarcSchemaTests.java | 2 +- 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java index ddbade4ec2..820f6e92f0 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java @@ -292,7 +292,8 @@ public ScimUser dtoFromEntity(IamAccount entity) { if (scimProperties.isEnableAarc()) { builder.voPersonId(entity.getUuid() + "@" + iamProperties.getOrganisation().getName()); builder.organizationName(iamProperties.getOrganisation().getName()); - builder.addAarcName(new ScimAarcName(getScimName(entity))); + builder.aarcDisplayName(entity.getUserInfo().getName()); + builder.aarcName(new ScimAarcName(getScimName(entity))); builder.addAarcEmail(entity.getUserInfo().getEmail()); if (entity.hasAffiliation()) { diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcName.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcName.java index 5928d09532..c38f6743d5 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcName.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcName.java @@ -16,20 +16,14 @@ package it.infn.mw.iam.api.scim.model; public class ScimAarcName { - private final String displayName; private final String givenName; private final String familyName; public ScimAarcName(ScimName name) { - this.displayName = name.getFormatted(); this.givenName = name.getGivenName(); this.familyName = name.getFamilyName(); } - public String getDisplayName() { - return displayName; - } - public String getGivenName() { return givenName; } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java index d9efc76004..a88c0ffa6d 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java @@ -24,11 +24,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; @JsonInclude(JsonInclude.Include.NON_EMPTY) -@JsonIgnoreProperties(value = {"voPersonId", "name", "email", "organizationName", +@JsonIgnoreProperties(value = {"voPersonId", "displayName", "name", "email", "organizationName", "schacHomeOrganization", "voPersonExternalAffiliation", "assurance"}, allowGetters = true) public class ScimAarcUser { private final String voPersonId; + private final String displayName; private final ScimAarcName name; private final String email; private final String organizationName; @@ -39,7 +40,8 @@ public class ScimAarcUser { @JsonCreator private ScimAarcUser(@JsonProperty("voPersonId") String voPersonId, - @JsonProperty("name") ScimAarcName name, @JsonProperty("email") String email, + @JsonProperty("displayName") String displayName, @JsonProperty("name") ScimAarcName name, + @JsonProperty("email") String email, @JsonProperty("organizationName") String organizationName, @JsonProperty("schacHomeOrganization") String schacHomeOrganization, @JsonProperty("voPersonExternalAffiliations") List voPersonExternalAffiliations, @@ -47,6 +49,7 @@ private ScimAarcUser(@JsonProperty("voPersonId") String voPersonId, @JsonProperty("entitlements") List entitlements) { this.voPersonId = voPersonId; + this.displayName = displayName; this.name = name; this.email = email; this.organizationName = organizationName; @@ -58,6 +61,7 @@ private ScimAarcUser(@JsonProperty("voPersonId") String voPersonId, private ScimAarcUser(Builder b) { this.voPersonId = b.voPersonId; + this.displayName = b.displayName; this.name = b.name; this.email = b.email; this.organizationName = b.organizationName; @@ -71,6 +75,10 @@ public String getVoPersonId() { return voPersonId; } + public String getDisplayName() { + return displayName; + } + public String getOrganizationName() { return organizationName; } @@ -106,6 +114,7 @@ public static Builder builder() { public static class Builder { private String voPersonId; + private String displayName; private ScimAarcName name; private String email; private String organizationName; @@ -119,6 +128,11 @@ public Builder voPersonId(String voPersonId) { return this; } + public Builder displayName(String displayName) { + this.displayName = displayName; + return this; + } + public Builder name(ScimAarcName name) { this.name = name; return this; diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java index 5d4e77baf7..2bf2468386 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java @@ -611,6 +611,16 @@ public Builder voPersonId(String voPersonId) { return this; } + public Builder aarcDisplayName(String displayName) { + aarcUserBuilder.displayName(displayName); + return this; + } + + public Builder aarcName(ScimAarcName name) { + aarcUserBuilder.name(name); + return this; + } + public Builder organizationName(String organizationName) { aarcUserBuilder.organizationName(organizationName); return this; @@ -621,11 +631,6 @@ public Builder addVoPersonExternalAffiliation(ScimAffiliation value) { return this; } - public Builder addAarcName(ScimAarcName name) { - aarcUserBuilder.name(name); - return this; - } - public Builder addAarcEmail(String email) { aarcUserBuilder.email(email); return this; diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/user/ScimAarcSchemaTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/user/ScimAarcSchemaTests.java index 37a4d568c6..4ba8cca465 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/user/ScimAarcSchemaTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/user/ScimAarcSchemaTests.java @@ -63,7 +63,7 @@ void testScimAarcUserSchema() throws Exception { .andExpect(jsonPath("$.Resources[0]['" + ScimConstants.AARC_USER_SCHEMA + "'].voPersonId", equalTo(ACCOUNT_UUID + "@indigo-dc"))) .andExpect( - jsonPath("$.Resources[0]['" + ScimConstants.AARC_USER_SCHEMA + "'].name.displayName", + jsonPath("$.Resources[0]['" + ScimConstants.AARC_USER_SCHEMA + "'].displayName", equalTo(account.getUserInfo().getName()))) .andExpect( jsonPath("$.Resources[0]['" + ScimConstants.AARC_USER_SCHEMA + "'].name.familyName",