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..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,28 +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) { + public GroupConverter(ScimResourceLocationProvider rlp, ScimProperties scimProperties, + IamAccountRepository accountRepo, IamAccountService accountService) { this.resourceLocationProvider = rlp; + this.scimProperties = scimProperties; + this.accountRepo = accountRepo; + this.accountService = accountService; } /** @@ -103,11 +120,31 @@ 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()) { + 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/converter/UserConverter.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java index 426e006950..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 @@ -18,18 +18,26 @@ 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; 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.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; 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; @@ -46,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; @@ -57,20 +71,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 +256,7 @@ public ScimUser dtoFromEntity(IamAccount entity) { builder.affiliation(entity.getAffiliation()); } - for (LabelDescriptor ld : properties.getIncludeLabels()) { + for (LabelDescriptor ld : scimProperties.getIncludeLabels()) { entity.getLabelByPrefixAndName(ld.getPrefix(), ld.getName()) .ifPresent(el -> builder.addLabel(ScimLabel.builder() .withPrefix(el.getPrefix()) @@ -248,7 +265,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 +273,7 @@ public ScimUser dtoFromEntity(IamAccount entity) { .build())); } - if (properties.isIncludeManagedGroups()) { + if (scimProperties.isIncludeManagedGroups()) { groupManagerService.getManagedGroupInfoForAccount(entity) .getManagedGroups() .forEach(mg -> builder.addManagedGroup(ScimGroupRef.builder() @@ -266,10 +283,33 @@ public ScimUser dtoFromEntity(IamAccount entity) { .build())); } - if (properties.isIncludeAuthorities()) { + if (scimProperties.isIncludeAuthorities()) { entity.getAuthorities().forEach(a -> builder.addAuthority(a.getAuthority())); } + builder.enableAarc(scimProperties.isEnableAarc()); + + if (scimProperties.isEnableAarc()) { + builder.voPersonId(entity.getUuid() + "@" + iamProperties.getOrganisation().getName()); + builder.organizationName(iamProperties.getOrganisation().getName()); + builder.aarcDisplayName(entity.getUserInfo().getName()); + builder.aarcName(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))); + } + return builder.build(); } @@ -319,4 +359,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/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/ScimAarcName.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcName.java new file mode 100644 index 0000000000..c38f6743d5 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcName.java @@ -0,0 +1,34 @@ +/** + * 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 givenName; + private final String familyName; + + public ScimAarcName(ScimName name) { + this.givenName = name.getGivenName(); + this.familyName = name.getFamilyName(); + } + + 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 new file mode 100644 index 0000000000..a88c0ffa6d --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAarcUser.java @@ -0,0 +1,175 @@ +/** + * 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 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", "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; + private final String schacHomeOrganization; + private final List voPersonExternalAffiliations; + private final List assurance; + private final List entitlements; + + @JsonCreator + private ScimAarcUser(@JsonProperty("voPersonId") String voPersonId, + @JsonProperty("displayName") String displayName, @JsonProperty("name") ScimAarcName name, + @JsonProperty("email") String email, + @JsonProperty("organizationName") String organizationName, + @JsonProperty("schacHomeOrganization") String schacHomeOrganization, + @JsonProperty("voPersonExternalAffiliations") List voPersonExternalAffiliations, + @JsonProperty("assurance") List assurance, + @JsonProperty("entitlements") List entitlements) { + + this.voPersonId = voPersonId; + this.displayName = displayName; + this.name = name; + this.email = email; + 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.displayName = b.displayName; + this.name = b.name; + this.email = b.email; + 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 getDisplayName() { + return displayName; + } + + public String getOrganizationName() { + return organizationName; + } + + public ScimAarcName getName() { + return name; + } + + public String getEmail() { + return email; + } + + 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 displayName; + private ScimAarcName name; + private String email; + private String organizationName; + private String schacHomeOrganization; + private List voPersonExternalAffiliations = new LinkedList<>(); + private List assurance = new LinkedList<>(); + private List entitlements = new LinkedList<>(); + + public Builder voPersonId(String voPersonId) { + this.voPersonId = voPersonId; + return this; + } + + public Builder displayName(String displayName) { + this.displayName = displayName; + 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; + } + + public Builder schacHomeOrganization(String schacHomeOrganization) { + this.schacHomeOrganization = schacHomeOrganization; + return this; + } + + public Builder addVoPersonExternalAffiliation(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..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 @@ -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,7 @@ public static class Builder extends ScimResource.Builder { private String displayName; private Set members = new HashSet<>(); private ScimIndigoGroup indigoGroup = null; + private ScimAarcGroup aarcGroup = null; public Builder(String displayName) { super(); @@ -104,6 +113,16 @@ public Builder(String displayName) { indigoGroup = ScimIndigoGroup.getBuilder().build(); } + public Builder enableAarc(boolean 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 +146,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); 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 b18a4097a4..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 @@ -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.enableAarc && !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,6 +336,8 @@ 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(); + private boolean enableAarc = false; public Builder() { super(); @@ -326,6 +345,16 @@ public Builder() { schemas.add(INDIGO_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) { this(); this.userName = userName; @@ -577,10 +606,49 @@ public Builder addManagedGroup(ScimGroupRef groupRef) { return this; } + public Builder voPersonId(String voPersonId) { + aarcUserBuilder.voPersonId(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; + } + + public Builder addVoPersonExternalAffiliation(ScimAffiliation value) { + aarcUserBuilder.addVoPersonExternalAffiliation(value); + return this; + } + + public Builder addAarcEmail(String email) { + aarcUserBuilder.email(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); } - } } 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 446811b150..1785b5b0a0 100644 --- a/iam-login-service/src/main/resources/application.yml +++ b/iam-login-service/src/main/resources/application.yml @@ -292,3 +292,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} 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/converter/UserConverterTest.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/converter/UserConverterTests.java similarity index 81% 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 aa6daa12ba..1cb35872c7 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 @@ -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; @@ -34,12 +36,14 @@ 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.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; @@ -56,17 +60,22 @@ class UserConverterTest { @Mock private AccountGroupManagerService groupManagerService; @Mock - private ScimProperties properties; + 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"); - userConverter = - new UserConverter(properties, resourceLocationProvider, addressConverter, oidcIdConverter, - sshKeyConverter, samlIdConverter, x509CertificateIamConverter, groupManagerService); + 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); } @Test @@ -79,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()); 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..5c06bbc896 --- /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") +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"))))); + } +} 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..4ba8cca465 --- /dev/null +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/user/ScimAarcSchemaTests.java @@ -0,0 +1,81 @@ +/** + * 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.equalTo; +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.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}, + webEnvironment = WebEnvironment.MOCK) +@AutoConfigureMockMvc +@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 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]['" + ScimConstants.AARC_USER_SCHEMA + "'].voPersonId", + equalTo(ACCOUNT_UUID + "@indigo-dc"))) + .andExpect( + jsonPath("$.Resources[0]['" + ScimConstants.AARC_USER_SCHEMA + "'].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))); + } +} 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 7306f9777e..b9fb67b373 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 @@ -364,8 +364,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); } }