From 3b14d18ed46711739ef4dc58d55a873997d6275d Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 7 Mar 2024 13:20:46 +0100 Subject: [PATCH 01/25] Add alias properties to JSON deserialization of ScimUser class --- .../identity/uaa/scim/impl/ScimUserJsonDeserializer.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/scim/impl/ScimUserJsonDeserializer.java b/model/src/main/java/org/cloudfoundry/identity/uaa/scim/impl/ScimUserJsonDeserializer.java index d9524a7dc5f..fcd897f054b 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/scim/impl/ScimUserJsonDeserializer.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/scim/impl/ScimUserJsonDeserializer.java @@ -87,6 +87,10 @@ public ScimUser deserialize(JsonParser jp, DeserializationContext ctxt) throws I user.setOrigin(jp.readValueAs(String.class)); } else if ("zoneId".equalsIgnoreCase(fieldName)) { user.setZoneId(jp.readValueAs(String.class)); + } else if ("aliasId".equalsIgnoreCase(fieldName)) { + user.setAliasId(jp.readValueAs(String.class)); + } else if ("aliasZid".equalsIgnoreCase(fieldName)) { + user.setAliasZid(jp.readValueAs(String.class)); } else if ("salt".equalsIgnoreCase(fieldName)) { user.setSalt(jp.readValueAs(String.class)); } else if ("passwordLastModified".equalsIgnoreCase(fieldName)) { From 2b3ba5e29c852ad297933781c0c08be7c824008f Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 7 Mar 2024 13:33:29 +0100 Subject: [PATCH 02/25] Add alias handler for SCIM users --- .../uaa/alias/EntityAliasHandler.java | 17 ++ .../uaa/scim/ScimUserAliasHandler.java | 156 ++++++++++++++++++ .../uaa/scim/ScimUserProvisioning.java | 2 + .../scim/jdbc/JdbcScimUserProvisioning.java | 10 ++ 4 files changed, 185 insertions(+) create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandler.java b/server/src/main/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandler.java index 6636310f3ae..add11d43bad 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandler.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandler.java @@ -3,6 +3,7 @@ import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; import static org.springframework.util.StringUtils.hasText; +import java.util.Objects; import java.util.Optional; import org.cloudfoundry.identity.uaa.EntityWithAlias; @@ -233,4 +234,20 @@ public final Optional retrieveAliasEntity(final T originalEntity) { protected abstract T updateEntity(final T entity, final String zoneId); protected abstract T createEntity(final T entity, final String zoneId) throws EntityAliasFailedException; + + protected static boolean isValidAliasPair(final T entity1, final T entity2) { + // check if both entities have an alias + final boolean entity1HasAlias = hasText(entity1.getAliasId()) && hasText(entity1.getAliasZid()); + final boolean entity2HasAlias = hasText(entity2.getAliasId()) && hasText(entity2.getAliasZid()); + if (!entity1HasAlias || !entity2HasAlias) { + return false; + } + + // check if they reference each other + final boolean entity1ReferencesEntity2 = Objects.equals(entity1.getAliasId(), entity2.getId()) && + Objects.equals(entity1.getAliasZid(), entity2.getZoneId()); + final boolean entity2ReferencesEntity1 = Objects.equals(entity2.getAliasId(), entity1.getId()) && + Objects.equals(entity2.getAliasZid(), entity1.getZoneId()); + return entity1ReferencesEntity2 && entity2ReferencesEntity1; + } } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java new file mode 100644 index 00000000000..496ca680d2b --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java @@ -0,0 +1,156 @@ +package org.cloudfoundry.identity.uaa.scim; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; + +import java.util.Optional; + +import org.cloudfoundry.identity.uaa.alias.EntityAliasFailedException; +import org.cloudfoundry.identity.uaa.alias.EntityAliasHandler; +import org.cloudfoundry.identity.uaa.provider.IdentityProvider; +import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; +import org.cloudfoundry.identity.uaa.scim.exception.ScimException; +import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceAlreadyExistsException; +import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceNotFoundException; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; +import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +@Component +public class ScimUserAliasHandler extends EntityAliasHandler { + private final ScimUserProvisioning scimUserProvisioning; + private final IdentityProviderProvisioning identityProviderProvisioning; + private final IdentityZoneManager identityZoneManager; + + protected ScimUserAliasHandler( + @Qualifier("identityZoneProvisioning") final IdentityZoneProvisioning identityZoneProvisioning, + final ScimUserProvisioning scimUserProvisioning, + final IdentityProviderProvisioning identityProviderProvisioning, + final IdentityZoneManager identityZoneManager, + @Value("${login.aliasEntitiesEnabled:false}") final boolean aliasEntitiesEnabled + ) { + super(identityZoneProvisioning, aliasEntitiesEnabled); + this.scimUserProvisioning = scimUserProvisioning; + this.identityProviderProvisioning = identityProviderProvisioning; + this.identityZoneManager = identityZoneManager; + } + + @Override + protected boolean additionalValidationChecksForNewAlias(final ScimUser requestBody) { + /* check if an IdP with the user's origin exists in both the current and the alias zone and that they are + * aliases of each other */ + final IdentityProvider idpInAliasZone = retrieveIdpByOrigin( + requestBody.getOrigin(), + requestBody.getAliasZid() + ); + final IdentityProvider idpInCurrentZone = retrieveIdpByOrigin( + requestBody.getOrigin(), + identityZoneManager.getCurrentIdentityZoneId() + ); + return EntityAliasHandler.isValidAliasPair(idpInCurrentZone, idpInAliasZone); + } + + private IdentityProvider retrieveIdpByOrigin(final String originKey, final String zoneId) { + final IdentityProvider idpInAliasZone; + try { + idpInAliasZone = identityProviderProvisioning.retrieveByOrigin( + originKey, + zoneId + ); + } catch (final EmptyResultDataAccessException e) { + throw new ScimException( + String.format("No IdP with the origin '%s' exists in the zone '%s'.", originKey, zoneId), + HttpStatus.BAD_REQUEST + ); + } + return idpInAliasZone; + } + + @Override + protected void setId(final ScimUser entity, final String id) { + entity.setId(id); + } + + @Override + protected void setZoneId(final ScimUser entity, final String zoneId) { + entity.setZoneId(zoneId); + } + + @Override + protected ScimUser cloneEntity(final ScimUser originalEntity) { + final ScimUser aliasUser = new ScimUser(); + + aliasUser.setUserName(originalEntity.getUserName()); + aliasUser.setUserType(originalEntity.getUserType()); + + aliasUser.setOrigin(originalEntity.getOrigin()); + aliasUser.setExternalId(originalEntity.getExternalId()); + + aliasUser.setTitle(originalEntity.getTitle()); + aliasUser.setName(originalEntity.getName()); + aliasUser.setDisplayName(originalEntity.getDisplayName()); + aliasUser.setNickName(originalEntity.getNickName()); + + aliasUser.setEmails(originalEntity.getEmails()); + aliasUser.setPhoneNumbers(originalEntity.getPhoneNumbers()); + + aliasUser.setLocale(originalEntity.getLocale()); + aliasUser.setTimezone(originalEntity.getTimezone()); + aliasUser.setProfileUrl(originalEntity.getProfileUrl()); + + final String passwordOriginalEntity = scimUserProvisioning.retrievePasswordForUser( + originalEntity.getId(), + originalEntity.getZoneId() + ); + aliasUser.setPassword(passwordOriginalEntity); + aliasUser.setSalt(originalEntity.getSalt()); + aliasUser.setPasswordLastModified(originalEntity.getPasswordLastModified()); + aliasUser.setLastLogonTime(originalEntity.getLastLogonTime()); + + aliasUser.setActive(originalEntity.isActive()); + aliasUser.setVerified(originalEntity.isVerified()); + + // the alias user won't have any groups or approvals in the alias zone, they need to be assigned separately + aliasUser.setApprovals(emptySet()); + aliasUser.setGroups(emptyList()); + + aliasUser.setSchemas(originalEntity.getSchemas()); + + // aliasId, aliasZid, id and zoneId are set in the parent class + + return aliasUser; + } + + @Override + protected Optional retrieveEntity(final String id, final String zoneId) { + final ScimUser user; + try { + user = scimUserProvisioning.retrieve(id, zoneId); + } catch (final ScimResourceNotFoundException e) { + return Optional.empty(); + } + return Optional.ofNullable(user); + } + + @Override + protected ScimUser updateEntity(final ScimUser entity, final String zoneId) { + return scimUserProvisioning.update(entity.getId(), entity, zoneId); + } + + @Override + protected ScimUser createEntity(final ScimUser entity, final String zoneId) { + try { + return scimUserProvisioning.createUser(entity, entity.getPassword(), zoneId); + } catch (final ScimResourceAlreadyExistsException e) { + final String errorMessage = String.format( + "Could not create %s. A user with the same username already exists in the alias zone.", + entity.getAliasDescription() + ); + throw new EntityAliasFailedException(errorMessage, HttpStatus.CONFLICT.value(), e); + } + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserProvisioning.java index 93965e0f219..058de779fce 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserProvisioning.java @@ -31,6 +31,8 @@ public interface ScimUserProvisioning extends ResourceManager, Queryab List retrieveByUsernameAndOriginAndZone(String username, String origin, String zoneId); + String retrievePasswordForUser(String id, String zoneId); + void changePassword(String id, String oldPassword, String newPassword, String zoneId) throws ScimResourceNotFoundException; void updatePasswordChangeRequired(String userId, boolean passwordChangeRequired, String zoneId) throws ScimResourceNotFoundException; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioning.java index 6ad15f7f379..182e143e36c 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioning.java @@ -160,6 +160,16 @@ public ScimUser retrieve(String id, String zoneId) { } } + @Override + public String retrievePasswordForUser(final String id, final String zoneId) { + return jdbcTemplate.queryForObject( + READ_PASSWORD_SQL, + new Object[]{id, zoneId}, + new int[]{VARCHAR, VARCHAR}, + String.class + ); + } + @Override public List retrieveByEmailAndZone(String email, String origin, String zoneId) { return jdbcTemplate.query(USER_BY_EMAIL_AND_ORIGIN_AND_ZONE_QUERY, mapper, email, origin, zoneId); From aa22118c72d6d433d3086f57a1a3d5d39dce4835 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 3 Apr 2024 11:42:20 +0200 Subject: [PATCH 03/25] Refactor --- .../identity/uaa/scim/ScimUserAliasHandler.java | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java index 496ca680d2b..0fc39a11478 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java @@ -43,24 +43,16 @@ protected ScimUserAliasHandler( protected boolean additionalValidationChecksForNewAlias(final ScimUser requestBody) { /* check if an IdP with the user's origin exists in both the current and the alias zone and that they are * aliases of each other */ - final IdentityProvider idpInAliasZone = retrieveIdpByOrigin( - requestBody.getOrigin(), - requestBody.getAliasZid() - ); - final IdentityProvider idpInCurrentZone = retrieveIdpByOrigin( - requestBody.getOrigin(), - identityZoneManager.getCurrentIdentityZoneId() - ); + final String origin = requestBody.getOrigin(); + final IdentityProvider idpInAliasZone = retrieveIdpByOrigin(origin, requestBody.getAliasZid()); + final IdentityProvider idpInCurrentZone = retrieveIdpByOrigin(origin, identityZoneManager.getCurrentIdentityZoneId()); return EntityAliasHandler.isValidAliasPair(idpInCurrentZone, idpInAliasZone); } private IdentityProvider retrieveIdpByOrigin(final String originKey, final String zoneId) { final IdentityProvider idpInAliasZone; try { - idpInAliasZone = identityProviderProvisioning.retrieveByOrigin( - originKey, - zoneId - ); + idpInAliasZone = identityProviderProvisioning.retrieveByOrigin(originKey, zoneId); } catch (final EmptyResultDataAccessException e) { throw new ScimException( String.format("No IdP with the origin '%s' exists in the zone '%s'.", originKey, zoneId), From 800433e64d6da3a384545871e16fbf031f391bae Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 4 Apr 2024 17:22:36 +0200 Subject: [PATCH 04/25] Add @NonNull annotations to EntityAliasHandler.isValidAliasPair parameters --- .../cloudfoundry/identity/uaa/alias/EntityAliasHandler.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandler.java b/server/src/main/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandler.java index 6b1b9656492..48730acf8eb 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandler.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandler.java @@ -206,7 +206,10 @@ public final Optional retrieveAliasEntity(final T originalEntity) { protected abstract T createEntity(final T entity, final String zoneId) throws EntityAliasFailedException; - protected static boolean isValidAliasPair(final T entity1, final T entity2) { + protected static boolean isValidAliasPair( + @NonNull final T entity1, + @NonNull final T entity2 + ) { // check if both entities have an alias final boolean entity1HasAlias = hasText(entity1.getAliasId()) && hasText(entity1.getAliasZid()); final boolean entity2HasAlias = hasText(entity2.getAliasId()) && hasText(entity2.getAliasZid()); From ced30264a45b8e6ede10153aafe60d20f64cf05d Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 4 Apr 2024 17:23:48 +0200 Subject: [PATCH 05/25] Add null check to IdPs in ScimUserAliasHandler.additionalValidationChecksForNewAlias --- .../uaa/scim/ScimUserAliasHandler.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java index 0fc39a11478..384a08baf3a 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java @@ -9,7 +9,6 @@ import org.cloudfoundry.identity.uaa.alias.EntityAliasHandler; import org.cloudfoundry.identity.uaa.provider.IdentityProvider; import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; -import org.cloudfoundry.identity.uaa.scim.exception.ScimException; import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceAlreadyExistsException; import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceNotFoundException; import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; @@ -44,22 +43,25 @@ protected boolean additionalValidationChecksForNewAlias(final ScimUser requestBo /* check if an IdP with the user's origin exists in both the current and the alias zone and that they are * aliases of each other */ final String origin = requestBody.getOrigin(); - final IdentityProvider idpInAliasZone = retrieveIdpByOrigin(origin, requestBody.getAliasZid()); - final IdentityProvider idpInCurrentZone = retrieveIdpByOrigin(origin, identityZoneManager.getCurrentIdentityZoneId()); - return EntityAliasHandler.isValidAliasPair(idpInCurrentZone, idpInAliasZone); + final Optional> idpInAliasZone = retrieveIdpByOrigin(origin, requestBody.getAliasZid()); + if (idpInAliasZone.isEmpty()) { + return false; + } + final Optional> idpInCurrentZone = retrieveIdpByOrigin(origin, identityZoneManager.getCurrentIdentityZoneId()); + if (idpInCurrentZone.isEmpty()) { + return false; + } + return EntityAliasHandler.isValidAliasPair(idpInCurrentZone.get(), idpInAliasZone.get()); } - private IdentityProvider retrieveIdpByOrigin(final String originKey, final String zoneId) { + private Optional> retrieveIdpByOrigin(final String originKey, final String zoneId) { final IdentityProvider idpInAliasZone; try { idpInAliasZone = identityProviderProvisioning.retrieveByOrigin(originKey, zoneId); } catch (final EmptyResultDataAccessException e) { - throw new ScimException( - String.format("No IdP with the origin '%s' exists in the zone '%s'.", originKey, zoneId), - HttpStatus.BAD_REQUEST - ); + return Optional.empty(); } - return idpInAliasZone; + return Optional.ofNullable(idpInAliasZone); } @Override From 688a5132f77bfa41ad9ad3c9687d6c6e7cbedda9 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 4 Apr 2024 17:24:12 +0200 Subject: [PATCH 06/25] Add tests for validation logic for ScimUsers --- .../ScimUserAliasHandlerValidationTest.java | 286 ++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java new file mode 100644 index 00000000000..a0dd39139cf --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java @@ -0,0 +1,286 @@ +package org.cloudfoundry.identity.uaa.scim; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.UUID; +import java.util.stream.Stream; + +import org.cloudfoundry.identity.uaa.alias.EntityAliasHandler; +import org.cloudfoundry.identity.uaa.alias.EntityAliasHandlerValidationTest; +import org.cloudfoundry.identity.uaa.provider.IdentityProvider; +import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; +import org.cloudfoundry.identity.uaa.util.AlphanumericRandomValueStringGenerator; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; +import org.cloudfoundry.identity.uaa.zone.ZoneDoesNotExistsException; +import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.dao.EmptyResultDataAccessException; + +@ExtendWith(MockitoExtension.class) +class ScimUserAliasHandlerValidationTest extends EntityAliasHandlerValidationTest { + private static final AlphanumericRandomValueStringGenerator RANDOM_STRING_GENERATOR = new AlphanumericRandomValueStringGenerator(5); + + @Mock + private IdentityZoneProvisioning identityZoneProvisioning; + @Mock + private ScimUserProvisioning scimUserProvisioning; + @Mock + private IdentityProviderProvisioning identityProviderProvisioning; + @Mock + private IdentityZoneManager identityZoneManager; + + @Override + protected EntityAliasHandler buildAliasHandler(final boolean aliasEntitiesEnabled) { + return new ScimUserAliasHandler( + identityZoneProvisioning, + scimUserProvisioning, + identityProviderProvisioning, + identityZoneManager, + aliasEntitiesEnabled + ); + } + + @Override + protected ScimUser buildEntityWithAliasProps(final String aliasId, final String aliasZid) { + final ScimUser scimUser = new ScimUser(); + + scimUser.setDisplayName("Some Displayname"); + scimUser.setPrimaryEmail("some.email@example.com"); + + scimUser.setPhoneNumbers(Collections.singletonList(new ScimUser.PhoneNumber("12345"))); + + scimUser.setAliasId(aliasId); + scimUser.setAliasZid(aliasZid); + + return scimUser; + } + + @Override + protected void changeNonAliasProperties(final ScimUser entity) { + entity.setNickName("some-new-nickname"); + } + + @Override + protected void setZoneId(final ScimUser entity, final String zoneId) { + entity.setZoneId(zoneId); + } + + @Override + protected void arrangeZoneExists(final String zoneId) { + if (!zoneId.equals(UAA)) { + return; + } + lenient().when(identityZoneProvisioning.retrieve(zoneId)).thenReturn(null); + } + + @Override + protected void arrangeZoneDoesNotExist(final String zoneId) { + when(identityZoneProvisioning.retrieve(zoneId)) + .thenThrow(new ZoneDoesNotExistsException("Zone does not exist.")); + } + + @Nested + class NoExistingAlias { + @Nested + class AliasFeatureEnabled extends NoExistingAlias_AliasFeatureEnabled { + private final String customZoneId = UUID.randomUUID().toString(); + + @ParameterizedTest + @MethodSource("provideExistingEntityArguments") + void shouldReturnFalse_IfIdpHasNoAlias_UaaToCustomZone( + final ExistingEntityArgument existingEntityArgument + ) { + shouldReturnFalse_IfIdpHasNoAlias(existingEntityArgument, UAA, customZoneId); + } + + @ParameterizedTest + @MethodSource("provideExistingEntityArguments") + void shouldReturnFalse_IfIdpHasNoAlias_CustomToUaaZone( + final ExistingEntityArgument existingEntityArgument + ) { + shouldReturnFalse_IfIdpHasNoAlias(existingEntityArgument, customZoneId, UAA); + } + + private void shouldReturnFalse_IfIdpHasNoAlias( + final ExistingEntityArgument existingEntityArgument, + final String zone1, + final String zone2 + ) { + arrangeZoneExists(zone1); + arrangeZoneExists(zone2); + + arrangeCurrentIdz(zone1); + + final ScimUser requestBody = buildEntityWithAliasProps(null, zone2); + requestBody.setZoneId(zone1); + final String origin = RANDOM_STRING_GENERATOR.generate(); + requestBody.setOrigin(origin); + + final ScimUser existingUser = resolveExistingEntityArgument(existingEntityArgument); + if (existingUser != null) { + existingUser.setZoneId(zone1); + existingUser.setOrigin(origin); + } + + // arrange IdP exists, but without alias + final IdentityProvider idp = buildIdp(UUID.randomUUID().toString(), origin, zone1, null, null); + arrangeIdpDoesNotExist(origin, zone2); + arrangeIdpExists(origin, zone1, idp); + + assertThat(aliasHandler.aliasPropertiesAreValid(requestBody, existingUser)).isFalse(); + } + + @ParameterizedTest + @MethodSource("provideExistingEntityArguments") + void shouldReturnFalse_IfIdpHasAliasToDifferentZoneThanUser( + final ExistingEntityArgument existingEntityArgument + ) { + arrangeZoneExists(customZoneId); + + // scenario only possible from UAA zone + arrangeCurrentIdz(UAA); + + final String aliasZidIdp = UUID.randomUUID().toString(); + arrangeZoneExists(aliasZidIdp); + + // should always be true + assertThat(aliasZidIdp).isNotEqualTo(customZoneId); + + final ScimUser requestBody = buildEntityWithAliasProps(null, customZoneId); + requestBody.setZoneId(UAA); + final String origin = RANDOM_STRING_GENERATOR.generate(); + requestBody.setOrigin(origin); + + final ScimUser existingUser = resolveExistingEntityArgument(existingEntityArgument); + if (existingUser != null) { + existingUser.setZoneId(UAA); + existingUser.setOrigin(origin); + } + + // arrange IdP exists with alias in different zone than the one referenced in the user + final String idpId = UUID.randomUUID().toString(); + final String aliasIdpId = UUID.randomUUID().toString(); + final IdentityProvider idp = buildIdp(idpId, origin, UAA, aliasIdpId, aliasZidIdp); + final IdentityProvider aliasIdp = buildIdp(aliasIdpId, origin, aliasZidIdp, idpId, UAA); + arrangeIdpExists(origin, UAA, idp); + arrangeIdpExists(origin, aliasZidIdp, aliasIdp); + + assertThat(aliasHandler.aliasPropertiesAreValid(requestBody, existingUser)).isFalse(); + } + + @ParameterizedTest + @MethodSource("provideExistingEntityArguments") + void shouldReturnTrue_IfIdpHasAliasToSameZoneAsUser_UaaToCustomZone( + final ExistingEntityArgument existingEntityArgument + ) { + shouldReturnTrue_IfIdpHasAliasToSameZoneAsUser(existingEntityArgument, UAA, customZoneId); + } + + @ParameterizedTest + @MethodSource("provideExistingEntityArguments") + void shouldReturnTrue_IfIdpHasAliasToSameZoneAsUser_CustomToUaaZone( + final ExistingEntityArgument existingEntityArgument + ) { + shouldReturnTrue_IfIdpHasAliasToSameZoneAsUser(existingEntityArgument, customZoneId, UAA); + } + + private void shouldReturnTrue_IfIdpHasAliasToSameZoneAsUser( + final ExistingEntityArgument existingEntityArgument, + final String zone1, + final String zone2 + ) { + arrangeZoneExists(zone1); + arrangeZoneExists(zone2); + + arrangeCurrentIdz(zone1); + + final ScimUser requestBody = buildEntityWithAliasProps(null, zone2); + requestBody.setZoneId(zone1); + final String origin = RANDOM_STRING_GENERATOR.generate(); + requestBody.setOrigin(origin); + + final ScimUser existingUser = resolveExistingEntityArgument(existingEntityArgument); + if (existingUser != null) { + existingUser.setZoneId(zone1); + existingUser.setOrigin(origin); + } + + // arrange IdP exists with alias in same zone as the one referenced in the user + final String idpId = UUID.randomUUID().toString(); + final String aliasIdpId = UUID.randomUUID().toString(); + final IdentityProvider idp = buildIdp(idpId, origin, zone1, aliasIdpId, zone2); + final IdentityProvider aliasIdp = buildIdp(aliasIdpId, origin, zone2, idpId, zone1); + arrangeIdpExists(origin, zone1, idp); + arrangeIdpExists(origin, zone2, aliasIdp); + + assertThat(aliasHandler.aliasPropertiesAreValid(requestBody, existingUser)).isTrue(); + } + + private void arrangeCurrentIdz(final String zoneId) { + lenient().when(identityZoneManager.getCurrentIdentityZoneId()).thenReturn(zoneId); + } + + private static IdentityProvider buildIdp( + final String id, + final String origin, + final String zoneId, + final String aliasId, + final String aliasZid + ) { + final IdentityProvider idp = new IdentityProvider<>(); + idp.setOriginKey(origin); + idp.setId(id); + idp.setName(origin); + idp.setIdentityZoneId(zoneId); + idp.setAliasId(aliasId); + idp.setAliasZid(aliasZid); + return idp; + } + + private void arrangeIdpDoesNotExist(final String origin, final String aliasZid) { + when(identityProviderProvisioning.retrieveByOrigin(origin, aliasZid)) + .thenThrow(new EmptyResultDataAccessException(1)); + } + + private void arrangeIdpExists( + final String origin, + final String zoneId, + final IdentityProvider idp + ) { + lenient().when(identityProviderProvisioning.retrieveByOrigin(origin, zoneId)).thenReturn(idp); + } + + private static Stream provideExistingEntityArguments() { + return existingEntityArgNoAlias().map(Arguments::of); + } + } + + @Nested + class AliasFeatureDisabled extends NoExistingAlias_AliasFeatureDisabled { + // all tests defined in superclass + } + } + + @Nested + class ExistingAlias { + @Nested + class AliasFeatureEnabled extends ExistingAlias_AliasFeatureEnabled { + // all tests defined in superclass + } + + @Nested + class AliasFeatureDisabled extends ExistingAlias_AliasFeatureDisabled { + // all tests defined in superclass + } + } +} \ No newline at end of file From 1a3668d414054e0b2cd471b83a5b7cb7746c13a6 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 10 Apr 2024 08:52:06 +0200 Subject: [PATCH 07/25] Adjust ScimUser properties set in ScimUserAliasHandler.cloneEntity --- .../uaa/scim/ScimUserAliasHandler.java | 53 +++++++++---------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java index 384a08baf3a..7c560ea84eb 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java @@ -1,7 +1,6 @@ package org.cloudfoundry.identity.uaa.scim; -import static java.util.Collections.emptyList; -import static java.util.Collections.emptySet; +import static org.cloudfoundry.identity.uaa.util.UaaStringUtils.EMPTY_STRING; import java.util.Optional; @@ -78,43 +77,39 @@ protected void setZoneId(final ScimUser entity, final String zoneId) { protected ScimUser cloneEntity(final ScimUser originalEntity) { final ScimUser aliasUser = new ScimUser(); - aliasUser.setUserName(originalEntity.getUserName()); - aliasUser.setUserType(originalEntity.getUserType()); + aliasUser.setId(null); + aliasUser.setExternalId(originalEntity.getExternalId()); + /* we only allow alias users to be created if their origin IdP has an alias to the same zone, therefore, an IdP + * with the same origin key will exist in the alias zone */ aliasUser.setOrigin(originalEntity.getOrigin()); - aliasUser.setExternalId(originalEntity.getExternalId()); - aliasUser.setTitle(originalEntity.getTitle()); - aliasUser.setName(originalEntity.getName()); - aliasUser.setDisplayName(originalEntity.getDisplayName()); - aliasUser.setNickName(originalEntity.getNickName()); + aliasUser.setUserName(originalEntity.getUserName()); + aliasUser.setName(new ScimUser.Name(originalEntity.getGivenName(), originalEntity.getFamilyName())); aliasUser.setEmails(originalEntity.getEmails()); aliasUser.setPhoneNumbers(originalEntity.getPhoneNumbers()); - aliasUser.setLocale(originalEntity.getLocale()); - aliasUser.setTimezone(originalEntity.getTimezone()); - aliasUser.setProfileUrl(originalEntity.getProfileUrl()); - - final String passwordOriginalEntity = scimUserProvisioning.retrievePasswordForUser( - originalEntity.getId(), - originalEntity.getZoneId() - ); - aliasUser.setPassword(passwordOriginalEntity); - aliasUser.setSalt(originalEntity.getSalt()); - aliasUser.setPasswordLastModified(originalEntity.getPasswordLastModified()); - aliasUser.setLastLogonTime(originalEntity.getLastLogonTime()); - aliasUser.setActive(originalEntity.isActive()); aliasUser.setVerified(originalEntity.isVerified()); - // the alias user won't have any groups or approvals in the alias zone, they need to be assigned separately - aliasUser.setApprovals(emptySet()); - aliasUser.setGroups(emptyList()); - - aliasUser.setSchemas(originalEntity.getSchemas()); - - // aliasId, aliasZid, id and zoneId are set in the parent class + // idzId and alias properties will be set later + aliasUser.setZoneId(null); + aliasUser.setAliasId(null); + aliasUser.setAliasZid(null); + + // these timestamps will be overwritten during creation + aliasUser.setPasswordLastModified(null); + aliasUser.setLastLogonTime(null); + aliasUser.setPreviousLogonTime(null); + + /* password: empty string + * - alias users are only allowed for IdPs that also have an alias + * - IdPs can only have an alias if they are of type SAML, OIDC or OAuth 2.0 + * - users with such IdPs as their origin always have an empty password + */ + aliasUser.setPassword(EMPTY_STRING); + aliasUser.setSalt(null); return aliasUser; } From 28e206ac710c9443f6afd2ad9ebfccb47ba3a69f Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 10 Apr 2024 08:55:00 +0200 Subject: [PATCH 08/25] Revert adding ScimUserProvisioning.retrievePasswordForUser method --- .../identity/uaa/scim/ScimUserProvisioning.java | 2 -- .../uaa/scim/jdbc/JdbcScimUserProvisioning.java | 10 ---------- 2 files changed, 12 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserProvisioning.java index 8abab11ce5c..59e8992d4ff 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserProvisioning.java @@ -41,8 +41,6 @@ List retrieveByScimFilterOnlyActive( List retrieveByUsernameAndOriginAndZone(String username, String origin, String zoneId); - String retrievePasswordForUser(String id, String zoneId); - void changePassword(String id, String oldPassword, String newPassword, String zoneId) throws ScimResourceNotFoundException; void updatePasswordChangeRequired(String userId, boolean passwordChangeRequired, String zoneId) throws ScimResourceNotFoundException; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioning.java index ddb494c8186..4378d54fbeb 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/jdbc/JdbcScimUserProvisioning.java @@ -168,16 +168,6 @@ public ScimUser retrieve(String id, String zoneId) { } } - @Override - public String retrievePasswordForUser(final String id, final String zoneId) { - return jdbcTemplate.queryForObject( - READ_PASSWORD_SQL, - new Object[]{id, zoneId}, - new int[]{VARCHAR, VARCHAR}, - String.class - ); - } - @Override public List retrieveByEmailAndZone(String email, String origin, String zoneId) { return jdbcTemplate.query(USER_BY_EMAIL_AND_ORIGIN_AND_ZONE_QUERY, mapper, email, origin, zoneId); From 091fbf2416472d9278286ce1aed3b41b5e22f8a5 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 10 Apr 2024 09:00:23 +0200 Subject: [PATCH 09/25] Revert adding alias properties to ScimUserJsonDeserializer.deserialize --- .../identity/uaa/scim/impl/ScimUserJsonDeserializer.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/scim/impl/ScimUserJsonDeserializer.java b/model/src/main/java/org/cloudfoundry/identity/uaa/scim/impl/ScimUserJsonDeserializer.java index fcd897f054b..d9524a7dc5f 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/scim/impl/ScimUserJsonDeserializer.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/scim/impl/ScimUserJsonDeserializer.java @@ -87,10 +87,6 @@ public ScimUser deserialize(JsonParser jp, DeserializationContext ctxt) throws I user.setOrigin(jp.readValueAs(String.class)); } else if ("zoneId".equalsIgnoreCase(fieldName)) { user.setZoneId(jp.readValueAs(String.class)); - } else if ("aliasId".equalsIgnoreCase(fieldName)) { - user.setAliasId(jp.readValueAs(String.class)); - } else if ("aliasZid".equalsIgnoreCase(fieldName)) { - user.setAliasZid(jp.readValueAs(String.class)); } else if ("salt".equalsIgnoreCase(fieldName)) { user.setSalt(jp.readValueAs(String.class)); } else if ("passwordLastModified".equalsIgnoreCase(fieldName)) { From 79cd7c72e5787bfeb1dc3c6bea9845a9fbc5724e Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 10 Apr 2024 14:26:51 +0200 Subject: [PATCH 10/25] Introduce superclass for tests for EntityAliasHandler.ensureConsistencyOfAliasEntity --- ...tityAliasHandlerEnsureConsistencyTest.java | 162 ++++++++++++ ...iderAliasHandlerEnsureConsistencyTest.java | 234 +++++------------- 2 files changed, 227 insertions(+), 169 deletions(-) create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java new file mode 100644 index 00000000000..59376056891 --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java @@ -0,0 +1,162 @@ +package org.cloudfoundry.identity.uaa.alias; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; + +import java.util.UUID; + +import org.cloudfoundry.identity.uaa.EntityWithAlias; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.lang.Nullable; + +public abstract class EntityAliasHandlerEnsureConsistencyTest { + protected abstract EntityAliasHandler buildAliasHandler(final boolean aliasEntitiesEnabled); + protected abstract T shallowCloneEntity(final T entity); + protected abstract T buildEntityWithAliasProperties(@Nullable final String aliasId, @Nullable final String aliasZid); + protected abstract void changeNonAliasProperties(final T entity); + protected abstract void arrangeZoneDoesNotExist(final String zoneId); + protected abstract void mockUpdateEntity(final String zoneId); + protected abstract void mockCreateEntity(final String newId, final String zoneId); + protected abstract void arrangeEntityDoesNotExist(final String id, final String zoneId); + + protected final String customZoneId = UUID.randomUUID().toString(); + + private abstract class Base { + protected EntityAliasHandler aliasHandler; + + @BeforeEach + final void setUp() { + final boolean aliasEntitiesEnabled = isAliasFeatureEnabled(); + this.aliasHandler = buildAliasHandler(aliasEntitiesEnabled); + } + + protected abstract boolean isAliasFeatureEnabled(); + } + + private abstract class NoExistingAliasBase extends Base { + @Test + final void shouldIgnore_AliasZidEmptyInOriginalIdp() { + final T originalEntity = buildEntityWithAliasProperties(null, null); + final T existingEntity = shallowCloneEntity(originalEntity); + changeNonAliasProperties(existingEntity); + + final T result = aliasHandler.ensureConsistencyOfAliasEntity(originalEntity, existingEntity); + assertThat(result).isEqualTo(originalEntity); + } + } + + protected abstract class NoExistingAlias_AliasFeatureEnabled extends NoExistingAliasBase { + @Override + protected final boolean isAliasFeatureEnabled() { + return true; + } + + @Test + final void shouldThrow_WhenAliasZidSetButZoneDoesNotExist() { + final T existingEntity = buildEntityWithAliasProperties(null, null); + final T originalEntity = shallowCloneEntity(existingEntity); + originalEntity.setAliasZid(customZoneId); + + arrangeZoneDoesNotExist(customZoneId); + + assertThatExceptionOfType(EntityAliasFailedException.class).isThrownBy(() -> + aliasHandler.ensureConsistencyOfAliasEntity(originalEntity, existingEntity) + ); + } + + @Test + final void shouldCreateNewAliasIdp_WhenAliasZoneExistsAndAliasPropertiesAreSet() { + final T existingEntity = buildEntityWithAliasProperties(null, null); + final T originalEntity = shallowCloneEntity(existingEntity); + originalEntity.setAliasZid(customZoneId); + + final String aliasEntityId = UUID.randomUUID().toString(); + mockCreateEntity(aliasEntityId, customZoneId); + mockUpdateEntity(UAA); + + final T result = aliasHandler.ensureConsistencyOfAliasEntity( + originalEntity, + existingEntity + ); + assertThat(result.getAliasId()).isEqualTo(aliasEntityId); + assertThat(result.getAliasZid()).isEqualTo(customZoneId); + } + } + + protected abstract class NoExistingAlias_AliasFeatureDisabled extends NoExistingAliasBase { + @Override + protected final boolean isAliasFeatureEnabled() { + return false; + } + + @Test + final void shouldThrow_WhenAliasZidSet() { + final T existingEntity = buildEntityWithAliasProperties(null, null); + final T originalEntity = shallowCloneEntity(existingEntity); + originalEntity.setAliasZid(customZoneId); + + assertThatIllegalStateException().isThrownBy(() -> + aliasHandler.ensureConsistencyOfAliasEntity(originalEntity, existingEntity) + ).withMessage("Trying to create a new alias while alias feature is disabled."); + } + } + + protected abstract class ExistingAlias_AliasFeatureEnabled extends Base { + @Override + protected final boolean isAliasFeatureEnabled() { + return true; + } + + @Test + final void shouldThrow_WhenReferencedAliasEntityAndAliasZoneDoNotExist() { + final String aliasIdpId = UUID.randomUUID().toString(); + + final T existingEntity = buildEntityWithAliasProperties(aliasIdpId, customZoneId); + final T originalEntity = shallowCloneEntity(existingEntity); + changeNonAliasProperties(originalEntity); + + arrangeEntityDoesNotExist(aliasIdpId, customZoneId); + arrangeZoneDoesNotExist(customZoneId); + + assertThatExceptionOfType(EntityAliasFailedException.class).isThrownBy(() -> + aliasHandler.ensureConsistencyOfAliasEntity(originalEntity, existingEntity) + ); + } + } + + protected abstract class ExistingAlias_AliasFeatureDisabled extends Base { + @Override + protected final boolean isAliasFeatureEnabled() { + return false; + } + + @Test + final void shouldThrow_EvenIfNoAliasPropertyIsChanged() { + final T existingEntity = buildEntityWithAliasProperties(UUID.randomUUID().toString(), customZoneId); + + final T originalEntity = shallowCloneEntity(existingEntity); + changeNonAliasProperties(originalEntity); + + assertThatIllegalStateException().isThrownBy(() -> + aliasHandler.ensureConsistencyOfAliasEntity(originalEntity, existingEntity) + ).withMessage("Performing update on entity with alias while alias feature is disabled."); + } + + @Test + final void shouldThrow_AliasPropertiesSetToNull() { + final T existingEntity = buildEntityWithAliasProperties(UUID.randomUUID().toString(), customZoneId); + + final T originalEntity = shallowCloneEntity(existingEntity); + changeNonAliasProperties(originalEntity); + originalEntity.setAliasId(null); + originalEntity.setAliasZid(null); + + assertThatIllegalStateException().isThrownBy(() -> + aliasHandler.ensureConsistencyOfAliasEntity(originalEntity, existingEntity) + ).withMessage("Performing update on entity with alias while alias feature is disabled."); + } + } +} diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandlerEnsureConsistencyTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandlerEnsureConsistencyTest.java index 53409737ab3..5a0640cedf7 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandlerEnsureConsistencyTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandlerEnsureConsistencyTest.java @@ -1,8 +1,6 @@ package org.cloudfoundry.identity.uaa.provider; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OIDC10; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; import static org.mockito.ArgumentMatchers.any; @@ -14,10 +12,10 @@ import java.util.Objects; import java.util.UUID; -import org.cloudfoundry.identity.uaa.alias.EntityAliasFailedException; +import org.cloudfoundry.identity.uaa.alias.EntityAliasHandler; +import org.cloudfoundry.identity.uaa.alias.EntityAliasHandlerEnsureConsistencyTest; import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; import org.cloudfoundry.identity.uaa.zone.ZoneDoesNotExistsException; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -25,36 +23,27 @@ import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; @ExtendWith(MockitoExtension.class) -public class IdentityProviderAliasHandlerEnsureConsistencyTest { +public class IdentityProviderAliasHandlerEnsureConsistencyTest extends EntityAliasHandlerEnsureConsistencyTest> { @Mock private IdentityZoneProvisioning identityZoneProvisioning; @Mock private IdentityProviderProvisioning identityProviderProvisioning; - private IdentityProviderAliasHandler idpAliasHandler; - private final String customZoneId = UUID.randomUUID().toString(); - - @BeforeEach - void setUp() { - idpAliasHandler = new IdentityProviderAliasHandler( + @Override + protected EntityAliasHandler> buildAliasHandler(final boolean aliasEntitiesEnabled) { + return new IdentityProviderAliasHandler( identityZoneProvisioning, identityProviderProvisioning, - false + aliasEntitiesEnabled ); } @Nested class ExistingAlias { @Nested - class AliasFeatureEnabled { - @BeforeEach - void setUp() { - arrangeAliasFeatureEnabled(true); - } - + class AliasFeatureEnabled extends ExistingAlias_AliasFeatureEnabled { @Test void shouldPropagateChangesToExistingAlias() { final String aliasIdpId = UUID.randomUUID().toString(); @@ -67,18 +56,18 @@ void shouldPropagateChangesToExistingAlias() { existingIdp.setAliasId(aliasIdpId); existingIdp.setAliasZid(customZoneId); - final IdentityProvider originalIdp = shallowCloneIdp(existingIdp); + final IdentityProvider originalIdp = shallowCloneEntity(existingIdp); final String newName = "some-new-name"; originalIdp.setName(newName); - final IdentityProvider aliasIdp = shallowCloneIdp(existingIdp); + final IdentityProvider aliasIdp = shallowCloneEntity(existingIdp); aliasIdp.setId(aliasIdpId); aliasIdp.setIdentityZoneId(customZoneId); aliasIdp.setAliasId(originalIdpId); aliasIdp.setAliasZid(UAA); when(identityProviderProvisioning.retrieve(aliasIdpId, customZoneId)).thenReturn(aliasIdp); - final IdentityProvider result = idpAliasHandler.ensureConsistencyOfAliasEntity( + final IdentityProvider result = aliasHandler.ensureConsistencyOfAliasEntity( originalIdp, existingIdp ); @@ -95,34 +84,6 @@ void shouldPropagateChangesToExistingAlias() { assertThat(capturedAliasIdp.getName()).isEqualTo(newName); } - @Test - void shouldThrow_WhenReferencedAliasIdpAndAliasZoneDoesNotExist() { - final String aliasIdpId = UUID.randomUUID().toString(); - final String originalIdpId = UUID.randomUUID().toString(); - - final IdentityProvider existingIdp = new IdentityProvider<>(); - existingIdp.setType(OIDC10); - existingIdp.setId(originalIdpId); - existingIdp.setIdentityZoneId(UAA); - existingIdp.setAliasId(aliasIdpId); - existingIdp.setAliasZid(customZoneId); - - final IdentityProvider originalIdp = shallowCloneIdp(existingIdp); - final String newName = "some-new-name"; - originalIdp.setName(newName); - - // dangling reference -> referenced alias IdP not present - when(identityProviderProvisioning.retrieve(aliasIdpId, customZoneId)).thenReturn(null); - - // alias zone does not exist - when(identityZoneProvisioning.retrieve(customZoneId)) - .thenThrow(new ZoneDoesNotExistsException("zone does not exist")); - - assertThatExceptionOfType(EntityAliasFailedException.class).isThrownBy(() -> - idpAliasHandler.ensureConsistencyOfAliasEntity(originalIdp, existingIdp) - ); - } - @Test void shouldFixDanglingReferenceByCreatingNewAliasIdp() { final String initialAliasIdpId = UUID.randomUUID().toString(); @@ -136,7 +97,7 @@ void shouldFixDanglingReferenceByCreatingNewAliasIdp() { existingIdp.setAliasId(initialAliasIdpId); existingIdp.setAliasZid(customZoneId); - final IdentityProvider requestBody = shallowCloneIdp(existingIdp); + final IdentityProvider requestBody = shallowCloneEntity(existingIdp); final String newName = "some-new-name"; requestBody.setName(newName); @@ -144,7 +105,7 @@ void shouldFixDanglingReferenceByCreatingNewAliasIdp() { when(identityProviderProvisioning.retrieve(initialAliasIdpId, customZoneId)).thenReturn(null); // mock alias IdP creation - final IdentityProvider createdAliasIdp = shallowCloneIdp(requestBody); + final IdentityProvider createdAliasIdp = shallowCloneEntity(requestBody); final String newAliasIdpId = UUID.randomUUID().toString(); createdAliasIdp.setId(newAliasIdpId); createdAliasIdp.setIdentityZoneId(customZoneId); @@ -159,7 +120,7 @@ void shouldFixDanglingReferenceByCreatingNewAliasIdp() { when(identityProviderProvisioning.update(argThat(new IdpWithAliasMatcher(UAA, originalIdpId, newAliasIdpId, customZoneId)), eq(UAA))) .then(invocationOnMock -> invocationOnMock.getArgument(0)); - final IdentityProvider result = idpAliasHandler.ensureConsistencyOfAliasEntity( + final IdentityProvider result = aliasHandler.ensureConsistencyOfAliasEntity( requestBody, existingIdp ); @@ -195,134 +156,27 @@ public boolean matches(final IdentityProvider argument) { } @Nested - class AliasFeatureDisabled { - @BeforeEach - void setUp() { - arrangeAliasFeatureEnabled(false); - } - - @Test - void shouldThrow_IfExistingEntityHasAlias() { - final String idpId = UUID.randomUUID().toString(); - final String aliasIdpId = UUID.randomUUID().toString(); - - final IdentityProvider existingIdp = new IdentityProvider<>(); - existingIdp.setType(OIDC10); - existingIdp.setId(idpId); - existingIdp.setIdentityZoneId(UAA); - existingIdp.setAliasId(aliasIdpId); - existingIdp.setAliasZid(customZoneId); - - final IdentityProvider originalIdp = shallowCloneIdp(existingIdp); - originalIdp.setAliasId(null); - originalIdp.setAliasZid(null); - originalIdp.setName("some-new-name"); - - assertThatIllegalStateException().isThrownBy(() -> - idpAliasHandler.ensureConsistencyOfAliasEntity(originalIdp, existingIdp) - ).withMessage("Performing update on entity with alias while alias feature is disabled."); - } + class AliasFeatureDisabled extends ExistingAlias_AliasFeatureDisabled { + // all tests defined in superclass } } @Nested class NoExistingAlias { - abstract class NoExistingAliasBase { - @Test - void shouldIgnore_AliasZidEmptyInOriginalIdp() { - final IdentityProvider existingIdp = new IdentityProvider<>(); - existingIdp.setType(OIDC10); - final String idpId = UUID.randomUUID().toString(); - existingIdp.setId(idpId); - existingIdp.setIdentityZoneId(UAA); - existingIdp.setAliasId(null); - existingIdp.setAliasZid(null); - - final IdentityProvider originalIdp = shallowCloneIdp(existingIdp); - originalIdp.setName("some-new-name"); - - final IdentityProvider result = idpAliasHandler.ensureConsistencyOfAliasEntity(originalIdp, existingIdp); - assertThat(result).isEqualTo(originalIdp); - } - } - @Nested - class AliasFeatureEnabled extends NoExistingAliasBase { - @BeforeEach - void setUp() { - arrangeAliasFeatureEnabled(true); - } - - @Test - void shouldThrow_WhenAliasZoneDoesNotExist() { - final IdentityProvider existingIdp = new IdentityProvider<>(); - existingIdp.setType(OIDC10); - final String idpId = UUID.randomUUID().toString(); - existingIdp.setId(idpId); - existingIdp.setIdentityZoneId(UAA); - existingIdp.setAliasId(null); - existingIdp.setAliasZid(null); - - final IdentityProvider requestBody = shallowCloneIdp(existingIdp); - requestBody.setAliasZid(customZoneId); - - when(identityZoneProvisioning.retrieve(customZoneId)) - .thenThrow(new ZoneDoesNotExistsException("zone does not exist")); - - assertThatExceptionOfType(EntityAliasFailedException.class).isThrownBy(() -> - idpAliasHandler.ensureConsistencyOfAliasEntity(requestBody, existingIdp) - ); - } - - @Test - void shouldCreateNewAliasIdp_WhenAliasZoneExistsAndAliasPropertiesAreSet() { - final IdentityProvider existingIdp = new IdentityProvider<>(); - existingIdp.setType(OIDC10); - final String idpId = UUID.randomUUID().toString(); - existingIdp.setId(idpId); - existingIdp.setIdentityZoneId(UAA); - existingIdp.setAliasId(null); - existingIdp.setAliasZid(null); - - final IdentityProvider requestBody = shallowCloneIdp(existingIdp); - requestBody.setAliasZid(customZoneId); - - final String aliasIdpId = UUID.randomUUID().toString(); - when(identityProviderProvisioning.create(any(), eq(customZoneId))).then(invocationOnMock -> { - final IdentityProvider idp = invocationOnMock.getArgument(0); - idp.setId(aliasIdpId); - return idp; - }); - - when(identityProviderProvisioning.update(any(), eq(UAA))) - .then(invocationOnMock -> invocationOnMock.getArgument(0)); - - final IdentityProvider result = idpAliasHandler.ensureConsistencyOfAliasEntity( - requestBody, - existingIdp - ); - assertThat(result.getAliasId()).isEqualTo(aliasIdpId); - assertThat(result.getAliasZid()).isEqualTo(customZoneId); - } + class AliasFeatureEnabled extends NoExistingAlias_AliasFeatureEnabled { + // all tests defined in superclass } @Nested - class AliasFeatureDisabled extends NoExistingAliasBase { - @BeforeEach - void setUp() { - arrangeAliasFeatureEnabled(false); - } + class AliasFeatureDisabled extends NoExistingAlias_AliasFeatureDisabled { + // all tests defined in superclass } } - private void arrangeAliasFeatureEnabled(final boolean enabled) { - ReflectionTestUtils.setField(idpAliasHandler, "aliasEntitiesEnabled", enabled); - } - - private static IdentityProvider shallowCloneIdp( - final IdentityProvider idp - ) { - final IdentityProvider cloneIdp = new IdentityProvider<>(); + @Override + protected IdentityProvider shallowCloneEntity(final IdentityProvider idp) { + final IdentityProvider cloneIdp = new IdentityProvider<>(); cloneIdp.setId(idp.getId()); cloneIdp.setName(idp.getName()); cloneIdp.setOriginKey(idp.getOriginKey()); @@ -337,4 +191,46 @@ private static IdentityProvider buildEntityWithAliasProperties(final String aliasId, final String aliasZid) { + final IdentityProvider existingIdp = new IdentityProvider<>(); + existingIdp.setType(OIDC10); + existingIdp.setId(UUID.randomUUID().toString()); + existingIdp.setIdentityZoneId(UAA); + existingIdp.setAliasId(aliasId); + existingIdp.setAliasZid(aliasZid); + return existingIdp; + } + + @Override + protected void changeNonAliasProperties(final IdentityProvider entity) { + entity.setName("some-new-name"); + } + + @Override + protected void arrangeZoneDoesNotExist(final String zoneId) { + when(identityZoneProvisioning.retrieve(zoneId)) + .thenThrow(new ZoneDoesNotExistsException("zone does not exist")); + } + + @Override + protected void mockUpdateEntity(final String zoneId) { + when(identityProviderProvisioning.update(any(), eq(zoneId))) + .then(invocationOnMock -> invocationOnMock.getArgument(0)); + } + + @Override + protected void mockCreateEntity(final String newId, final String zoneId) { + when(identityProviderProvisioning.create(any(), eq(customZoneId))).then(invocationOnMock -> { + final IdentityProvider idp = invocationOnMock.getArgument(0); + idp.setId(newId); + return idp; + }); + } + + @Override + protected void arrangeEntityDoesNotExist(final String id, final String zoneId) { + when(identityProviderProvisioning.retrieve(id, zoneId)).thenReturn(null); + } } From ef5f4dca94b59b12b68ca520497dfa2be63d5ac2 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 10 Apr 2024 14:33:21 +0200 Subject: [PATCH 11/25] Refactor --- .../EntityAliasHandlerEnsureConsistencyTest.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java index 59376056891..0b83b4b3a9c 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java @@ -38,7 +38,7 @@ final void setUp() { private abstract class NoExistingAliasBase extends Base { @Test - final void shouldIgnore_AliasZidEmptyInOriginalIdp() { + final void shouldIgnore_AliasZidEmptyInOriginalEntity() { final T originalEntity = buildEntityWithAliasProperties(null, null); final T existingEntity = shallowCloneEntity(originalEntity); changeNonAliasProperties(existingEntity); @@ -68,20 +68,20 @@ final void shouldThrow_WhenAliasZidSetButZoneDoesNotExist() { } @Test - final void shouldCreateNewAliasIdp_WhenAliasZoneExistsAndAliasPropertiesAreSet() { + final void shouldCreateNewAliasEntity_WhenAliasZoneExistsAndAliasPropertiesAreSet() { final T existingEntity = buildEntityWithAliasProperties(null, null); final T originalEntity = shallowCloneEntity(existingEntity); originalEntity.setAliasZid(customZoneId); - final String aliasEntityId = UUID.randomUUID().toString(); - mockCreateEntity(aliasEntityId, customZoneId); + final String aliasId = UUID.randomUUID().toString(); + mockCreateEntity(aliasId, customZoneId); mockUpdateEntity(UAA); final T result = aliasHandler.ensureConsistencyOfAliasEntity( originalEntity, existingEntity ); - assertThat(result.getAliasId()).isEqualTo(aliasEntityId); + assertThat(result.getAliasId()).isEqualTo(aliasId); assertThat(result.getAliasZid()).isEqualTo(customZoneId); } } @@ -112,13 +112,13 @@ protected final boolean isAliasFeatureEnabled() { @Test final void shouldThrow_WhenReferencedAliasEntityAndAliasZoneDoNotExist() { - final String aliasIdpId = UUID.randomUUID().toString(); + final String aliasId = UUID.randomUUID().toString(); - final T existingEntity = buildEntityWithAliasProperties(aliasIdpId, customZoneId); + final T existingEntity = buildEntityWithAliasProperties(aliasId, customZoneId); final T originalEntity = shallowCloneEntity(existingEntity); changeNonAliasProperties(originalEntity); - arrangeEntityDoesNotExist(aliasIdpId, customZoneId); + arrangeEntityDoesNotExist(aliasId, customZoneId); arrangeZoneDoesNotExist(customZoneId); assertThatExceptionOfType(EntityAliasFailedException.class).isThrownBy(() -> From 37eedad5991316940522b34a1565724c48896988 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 10 Apr 2024 14:43:51 +0200 Subject: [PATCH 12/25] Refactor --- ...iderAliasHandlerEnsureConsistencyTest.java | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandlerEnsureConsistencyTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandlerEnsureConsistencyTest.java index 5a0640cedf7..767b0cb6856 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandlerEnsureConsistencyTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandlerEnsureConsistencyTest.java @@ -47,14 +47,7 @@ class AliasFeatureEnabled extends ExistingAlias_AliasFeatureEnabled { @Test void shouldPropagateChangesToExistingAlias() { final String aliasIdpId = UUID.randomUUID().toString(); - final String originalIdpId = UUID.randomUUID().toString(); - - final IdentityProvider existingIdp = new IdentityProvider<>(); - existingIdp.setType(OIDC10); - existingIdp.setId(originalIdpId); - existingIdp.setIdentityZoneId(UAA); - existingIdp.setAliasId(aliasIdpId); - existingIdp.setAliasZid(customZoneId); + final IdentityProvider existingIdp = buildEntityWithAliasProperties(aliasIdpId, customZoneId); final IdentityProvider originalIdp = shallowCloneEntity(existingIdp); final String newName = "some-new-name"; @@ -63,6 +56,7 @@ void shouldPropagateChangesToExistingAlias() { final IdentityProvider aliasIdp = shallowCloneEntity(existingIdp); aliasIdp.setId(aliasIdpId); aliasIdp.setIdentityZoneId(customZoneId); + final String originalIdpId = existingIdp.getId(); aliasIdp.setAliasId(originalIdpId); aliasIdp.setAliasZid(UAA); when(identityProviderProvisioning.retrieve(aliasIdpId, customZoneId)).thenReturn(aliasIdp); @@ -87,15 +81,8 @@ void shouldPropagateChangesToExistingAlias() { @Test void shouldFixDanglingReferenceByCreatingNewAliasIdp() { final String initialAliasIdpId = UUID.randomUUID().toString(); - final String originalIdpId = UUID.randomUUID().toString(); - - final IdentityProvider existingIdp = new IdentityProvider<>(); - existingIdp.setType(OIDC10); - existingIdp.setConfig(new OIDCIdentityProviderDefinition()); - existingIdp.setId(originalIdpId); - existingIdp.setIdentityZoneId(UAA); - existingIdp.setAliasId(initialAliasIdpId); - existingIdp.setAliasZid(customZoneId); + final IdentityProvider existingIdp = buildEntityWithAliasProperties(initialAliasIdpId, customZoneId); + final String originalIdpId = existingIdp.getId(); final IdentityProvider requestBody = shallowCloneEntity(existingIdp); final String newName = "some-new-name"; From ff6079aab0c3927bf43a6d061655ab3e41b15ae7 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 10 Apr 2024 14:48:49 +0200 Subject: [PATCH 13/25] Move IdpWithAliasMatcher to superclass and make it generic --- ...tityAliasHandlerEnsureConsistencyTest.java | 22 ++++++++++++++ ...iderAliasHandlerEnsureConsistencyTest.java | 30 ++++--------------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java index 0b83b4b3a9c..663bd0b67e2 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java @@ -5,11 +5,13 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; +import java.util.Objects; import java.util.UUID; import org.cloudfoundry.identity.uaa.EntityWithAlias; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; import org.springframework.lang.Nullable; public abstract class EntityAliasHandlerEnsureConsistencyTest { @@ -159,4 +161,24 @@ final void shouldThrow_AliasPropertiesSetToNull() { ).withMessage("Performing update on entity with alias while alias feature is disabled."); } } + + protected static class EntityWithAliasMatcher implements ArgumentMatcher { + private final String zoneId; + private final String id; + private final String aliasId; + private final String aliasZid; + + public EntityWithAliasMatcher(final String zoneId, final String id, final String aliasId, final String aliasZid) { + this.zoneId = zoneId; + this.id = id; + this.aliasId = aliasId; + this.aliasZid = aliasZid; + } + + @Override + public boolean matches(final T argument) { + return Objects.equals(id, argument.getId()) && Objects.equals(zoneId, argument.getZoneId()) + && Objects.equals(aliasId, argument.getAliasId()) && Objects.equals(aliasZid, argument.getAliasZid()); + } + } } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandlerEnsureConsistencyTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandlerEnsureConsistencyTest.java index 767b0cb6856..61fec7d6ac5 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandlerEnsureConsistencyTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandlerEnsureConsistencyTest.java @@ -9,7 +9,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.util.Objects; import java.util.UUID; import org.cloudfoundry.identity.uaa.alias.EntityAliasHandler; @@ -20,7 +19,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -99,13 +97,15 @@ void shouldFixDanglingReferenceByCreatingNewAliasIdp() { createdAliasIdp.setAliasId(originalIdpId); createdAliasIdp.setAliasZid(UAA); when(identityProviderProvisioning.create( - argThat(new IdpWithAliasMatcher(customZoneId, null, originalIdpId, UAA)), + argThat(new EntityWithAliasMatcher>(customZoneId, null, originalIdpId, UAA)), eq(customZoneId) )).thenReturn(createdAliasIdp); // mock update of original IdP - when(identityProviderProvisioning.update(argThat(new IdpWithAliasMatcher(UAA, originalIdpId, newAliasIdpId, customZoneId)), eq(UAA))) - .then(invocationOnMock -> invocationOnMock.getArgument(0)); + when(identityProviderProvisioning.update( + argThat(new EntityWithAliasMatcher>(UAA, originalIdpId, newAliasIdpId, customZoneId)), + eq(UAA) + )).then(invocationOnMock -> invocationOnMock.getArgument(0)); final IdentityProvider result = aliasHandler.ensureConsistencyOfAliasEntity( requestBody, @@ -120,26 +120,6 @@ void shouldFixDanglingReferenceByCreatingNewAliasIdp() { final IdentityProvider updatedOriginalIdp = originalIdpCaptor.getValue(); assertThat(updatedOriginalIdp.getAliasId()).isEqualTo(newAliasIdpId); } - - private static class IdpWithAliasMatcher implements ArgumentMatcher> { - private final String identityZoneId; - private final String id; - private final String aliasId; - private final String aliasZid; - - public IdpWithAliasMatcher(final String identityZoneId, final String id, final String aliasId, final String aliasZid) { - this.identityZoneId = identityZoneId; - this.id = id; - this.aliasId = aliasId; - this.aliasZid = aliasZid; - } - - @Override - public boolean matches(final IdentityProvider argument) { - return Objects.equals(id, argument.getId()) && Objects.equals(identityZoneId, argument.getIdentityZoneId()) - && Objects.equals(aliasId, argument.getAliasId()) && Objects.equals(aliasZid, argument.getAliasZid()); - } - } } @Nested From b4fb27430dbd4fb6f348445182ee9fc26516705c Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 10 Apr 2024 16:47:03 +0200 Subject: [PATCH 14/25] Refactor --- ...tityAliasHandlerEnsureConsistencyTest.java | 19 ++++++++++++++ ...iderAliasHandlerEnsureConsistencyTest.java | 26 ++++++++++++------- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java index 663bd0b67e2..890da5889e5 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java @@ -16,12 +16,31 @@ public abstract class EntityAliasHandlerEnsureConsistencyTest { protected abstract EntityAliasHandler buildAliasHandler(final boolean aliasEntitiesEnabled); + protected abstract T shallowCloneEntity(final T entity); + protected abstract T buildEntityWithAliasProperties(@Nullable final String aliasId, @Nullable final String aliasZid); + + /** + * Change one or more properties (but neither 'aliasId' nor 'aliasZid') of the given entity. + */ protected abstract void changeNonAliasProperties(final T entity); + protected abstract void arrangeZoneDoesNotExist(final String zoneId); + + /** + * Mock updating entities by always returning the entity passed as an argument to the update method. + */ protected abstract void mockUpdateEntity(final String zoneId); + + /** + * Mock creating entities by taking the entity passed as an argument to the create method and setting the given new + * ID. + */ protected abstract void mockCreateEntity(final String newId, final String zoneId); + + protected abstract void arrangeEntityExists(final String id, final String zoneId, final T entity); + protected abstract void arrangeEntityDoesNotExist(final String id, final String zoneId); protected final String customZoneId = UUID.randomUUID().toString(); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandlerEnsureConsistencyTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandlerEnsureConsistencyTest.java index 61fec7d6ac5..d47901438ec 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandlerEnsureConsistencyTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandlerEnsureConsistencyTest.java @@ -51,13 +51,13 @@ void shouldPropagateChangesToExistingAlias() { final String newName = "some-new-name"; originalIdp.setName(newName); + // arrange alias IdP is present final IdentityProvider aliasIdp = shallowCloneEntity(existingIdp); - aliasIdp.setId(aliasIdpId); - aliasIdp.setIdentityZoneId(customZoneId); - final String originalIdpId = existingIdp.getId(); - aliasIdp.setAliasId(originalIdpId); - aliasIdp.setAliasZid(UAA); - when(identityProviderProvisioning.retrieve(aliasIdpId, customZoneId)).thenReturn(aliasIdp); + aliasIdp.setId(existingIdp.getAliasId()); + aliasIdp.setIdentityZoneId(existingIdp.getAliasZid()); + aliasIdp.setAliasId(existingIdp.getId()); + aliasIdp.setAliasZid(existingIdp.getZoneId()); + arrangeEntityExists(aliasIdp.getId(), aliasIdp.getZoneId(), aliasIdp); final IdentityProvider result = aliasHandler.ensureConsistencyOfAliasEntity( originalIdp, @@ -65,11 +65,11 @@ void shouldPropagateChangesToExistingAlias() { ); assertThat(result).isEqualTo(originalIdp); + // check if the change was propagated to the alias IdP final ArgumentCaptor aliasIdpArgumentCaptor = ArgumentCaptor.forClass(IdentityProvider.class); verify(identityProviderProvisioning).update(aliasIdpArgumentCaptor.capture(), eq(customZoneId)); - final IdentityProvider capturedAliasIdp = aliasIdpArgumentCaptor.getValue(); - assertThat(capturedAliasIdp.getAliasId()).isEqualTo(originalIdpId); + assertThat(capturedAliasIdp.getAliasId()).isEqualTo(existingIdp.getId()); assertThat(capturedAliasIdp.getAliasZid()).isEqualTo(UAA); assertThat(capturedAliasIdp.getId()).isEqualTo(aliasIdpId); assertThat(capturedAliasIdp.getIdentityZoneId()).isEqualTo(customZoneId); @@ -87,9 +87,9 @@ void shouldFixDanglingReferenceByCreatingNewAliasIdp() { requestBody.setName(newName); // dangling reference -> referenced alias IdP not present - when(identityProviderProvisioning.retrieve(initialAliasIdpId, customZoneId)).thenReturn(null); + arrangeEntityDoesNotExist(initialAliasIdpId, customZoneId); - // mock alias IdP creation + // mock creation of new alias IdP final IdentityProvider createdAliasIdp = shallowCloneEntity(requestBody); final String newAliasIdpId = UUID.randomUUID().toString(); createdAliasIdp.setId(newAliasIdpId); @@ -107,6 +107,7 @@ void shouldFixDanglingReferenceByCreatingNewAliasIdp() { eq(UAA) )).then(invocationOnMock -> invocationOnMock.getArgument(0)); + // check if the original IdP now references the new alias final IdentityProvider result = aliasHandler.ensureConsistencyOfAliasEntity( requestBody, existingIdp @@ -196,6 +197,11 @@ protected void mockCreateEntity(final String newId, final String zoneId) { }); } + @Override + protected void arrangeEntityExists(final String id, final String zoneId, final IdentityProvider entity) { + when(identityProviderProvisioning.retrieve(id, zoneId)).thenReturn(entity); + } + @Override protected void arrangeEntityDoesNotExist(final String id, final String zoneId) { when(identityProviderProvisioning.retrieve(id, zoneId)).thenReturn(null); From 463202f5cda5822917c9393cfc7cfbb2994be3ad Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 10 Apr 2024 16:48:25 +0200 Subject: [PATCH 15/25] Add method 'entitiesAreEqual' to later on support equality checks for ScimUsers --- .../alias/EntityAliasHandlerEnsureConsistencyTest.java | 9 ++++++++- ...dentityProviderAliasHandlerEnsureConsistencyTest.java | 6 ++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java index 890da5889e5..d181a77d9e4 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerEnsureConsistencyTest.java @@ -9,6 +9,7 @@ import java.util.UUID; import org.cloudfoundry.identity.uaa.EntityWithAlias; +import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatcher; @@ -43,6 +44,12 @@ public abstract class EntityAliasHandlerEnsureConsistencyTest entity1, final IdentityProvider entity2) { + return Objects.equals(entity1, entity2); + } } From 9ed15801179ee9eab0be386a6a018979ca3dc37f Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 10 Apr 2024 16:49:42 +0200 Subject: [PATCH 16/25] Add ScimUserAliasHandlerEnsureConsistencyTest --- ...UserAliasHandlerEnsureConsistencyTest.java | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerEnsureConsistencyTest.java diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerEnsureConsistencyTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerEnsureConsistencyTest.java new file mode 100644 index 00000000000..cc014eb79e8 --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerEnsureConsistencyTest.java @@ -0,0 +1,247 @@ +package org.cloudfoundry.identity.uaa.scim; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.Objects; +import java.util.UUID; + +import org.cloudfoundry.identity.uaa.alias.EntityAliasHandler; +import org.cloudfoundry.identity.uaa.alias.EntityAliasHandlerEnsureConsistencyTest; +import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; +import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceNotFoundException; +import org.cloudfoundry.identity.uaa.util.UaaStringUtils; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; +import org.cloudfoundry.identity.uaa.zone.ZoneDoesNotExistsException; +import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ScimUserAliasHandlerEnsureConsistencyTest extends EntityAliasHandlerEnsureConsistencyTest { + @Mock + private ScimUserProvisioning scimUserProvisioning; + @Mock + private IdentityProviderProvisioning identityProviderProvisioning; + @Mock + private IdentityZoneManager identityZoneManager; + @Mock + private IdentityZoneProvisioning identityZoneProvisioning; + + @Override + protected EntityAliasHandler buildAliasHandler(final boolean aliasEntitiesEnabled) { + return new ScimUserAliasHandler( + identityZoneProvisioning, + scimUserProvisioning, + identityProviderProvisioning, + identityZoneManager, + aliasEntitiesEnabled + ); + } + + @Override + protected ScimUser shallowCloneEntity(final ScimUser entity) { + final ScimUser clonedScimUser = new ScimUser(); + clonedScimUser.setId(entity.getId()); + clonedScimUser.setUserName(entity.getUserName()); + clonedScimUser.setPrimaryEmail(entity.getPrimaryEmail()); + clonedScimUser.setName(entity.getName()); + clonedScimUser.setActive(entity.isActive()); + clonedScimUser.setPhoneNumbers(entity.getPhoneNumbers()); + clonedScimUser.setOrigin(entity.getOrigin()); + clonedScimUser.setAliasId(entity.getAliasId()); + clonedScimUser.setAliasZid(entity.getAliasZid()); + clonedScimUser.setZoneId(entity.getZoneId()); + clonedScimUser.setPassword(entity.getPassword()); + clonedScimUser.setSalt(entity.getSalt()); + return clonedScimUser; + } + + @Override + protected ScimUser buildEntityWithAliasProperties(String aliasId, String aliasZid) { + final ScimUser scimUser = new ScimUser(); + scimUser.setId(UUID.randomUUID().toString()); + scimUser.setUserName("john.doe"); + scimUser.setPrimaryEmail("john.doe@example.com"); + scimUser.setName(new ScimUser.Name("John", "Doe")); + scimUser.setActive(true); + scimUser.setPhoneNumbers(Collections.singletonList(new ScimUser.PhoneNumber("12345"))); + scimUser.setOrigin("some-origin"); + scimUser.setAliasId(aliasId); + scimUser.setAliasZid(aliasZid); + scimUser.setZoneId(UAA); + scimUser.setPassword(""); + scimUser.setSalt(null); + return scimUser; + } + + @Override + protected boolean entitiesAreEqual(final ScimUser entity1, final ScimUser entity2) { + return Objects.equals(entity1.getId(), entity2.getId()) + && Objects.equals(entity1.getUserName(), entity2.getUserName()) + && Objects.equals(entity1.getPrimaryEmail(), entity2.getPrimaryEmail()) + && Objects.equals(entity1.getGivenName(), entity2.getGivenName()) + && Objects.equals(entity1.getFamilyName(), entity2.getFamilyName()) + && Objects.equals(entity1.isActive(), entity2.isActive()) + && Objects.equals(entity1.getPhoneNumbers(), entity2.getPhoneNumbers()) + && Objects.equals(entity1.getOrigin(), entity2.getOrigin()) + && Objects.equals(entity1.getAliasId(), entity2.getAliasId()) + && Objects.equals(entity1.getAliasZid(), entity2.getAliasZid()) + && Objects.equals(entity1.getZoneId(), entity2.getZoneId()) + && Objects.equals(entity1.getPassword(), entity2.getPassword()) + && Objects.equals(entity1.getSalt(), entity2.getSalt()); + } + + @Override + protected void changeNonAliasProperties(final ScimUser entity) { + entity.getName().setGivenName("some-new-given-name"); + } + + @Override + protected void arrangeZoneDoesNotExist(final String zoneId) { + when(identityZoneProvisioning.retrieve(zoneId)) + .thenThrow(new ZoneDoesNotExistsException("zone does not exist")); + } + + @Override + protected void mockUpdateEntity(final String zoneId) { + when(scimUserProvisioning.update(any(), any(), eq(zoneId))) + .then(invocationOnMock -> invocationOnMock.getArgument(1)); + } + + @Override + protected void mockCreateEntity(final String newId, final String zoneId) { + when(scimUserProvisioning.createUser(any(), anyString(), eq(zoneId))).then(invocationOnMock -> { + final ScimUser scimUser = invocationOnMock.getArgument(0); + scimUser.setId(newId); + return scimUser; + }); + } + + @Override + protected void arrangeEntityExists(final String id, final String zoneId, final ScimUser entity) { + when(scimUserProvisioning.retrieve(id, zoneId)).thenReturn(entity); + } + + @Override + protected void arrangeEntityDoesNotExist(final String id, final String zoneId) { + when(scimUserProvisioning.retrieve(id, zoneId)).thenThrow(new ScimResourceNotFoundException("user not found")); + } + + @Nested + class ExistingAlias { + @Nested + class AliasFeatureEnabled extends ExistingAlias_AliasFeatureEnabled { + @Test + void shouldPropagateChangesToExistingAlias() { + final String aliasId = UUID.randomUUID().toString(); + final ScimUser existingUser = buildEntityWithAliasProperties(aliasId, customZoneId); + + final ScimUser originalUser = shallowCloneEntity(existingUser); + final String newGivenName = "some-new-name"; + originalUser.setName(new ScimUser.Name(newGivenName, originalUser.getFamilyName())); + + // arrange alias user is present + final ScimUser aliasUser = shallowCloneEntity(existingUser); + aliasUser.setId(existingUser.getAliasId()); + aliasUser.setZoneId(existingUser.getAliasZid()); + aliasUser.setAliasId(existingUser.getId()); + aliasUser.setAliasZid(existingUser.getZoneId()); + arrangeEntityExists(aliasUser.getId(), aliasUser.getZoneId(), aliasUser); + + final ScimUser result = aliasHandler.ensureConsistencyOfAliasEntity( + originalUser, + existingUser + ); + assertThat(entitiesAreEqual(result, originalUser)).isTrue(); + + // check if the change was propagated to the alias user + final ArgumentCaptor userArgCaptor = ArgumentCaptor.forClass(ScimUser.class); + verify(scimUserProvisioning).update(eq(aliasId), userArgCaptor.capture(), eq(customZoneId)); + final ScimUser capturedUser = userArgCaptor.getValue(); + assertThat(capturedUser.getAliasId()).isEqualTo(existingUser.getId()); + assertThat(capturedUser.getAliasZid()).isEqualTo(UAA); + assertThat(capturedUser.getId()).isEqualTo(aliasId); + assertThat(capturedUser.getZoneId()).isEqualTo(customZoneId); + assertThat(capturedUser.getGivenName()).isEqualTo(newGivenName); + } + + @Test + void shouldFixDanglingReferenceByCreatingNewAliasEntity() { + final String initialAliasId = UUID.randomUUID().toString(); + final ScimUser existingUser = buildEntityWithAliasProperties(initialAliasId, customZoneId); + final String originalUserId = existingUser.getId(); + + final ScimUser requestBody = shallowCloneEntity(existingUser); + final String newGivenName = "some-new-given-name"; + requestBody.setName(new ScimUser.Name(newGivenName, requestBody.getFamilyName())); + + // dangling reference -> referenced alias user not present + arrangeEntityDoesNotExist(initialAliasId, customZoneId); + + // mock creation of new alias user + final ScimUser createdAliasUser = shallowCloneEntity(requestBody); + final String newAliasUserId = UUID.randomUUID().toString(); + createdAliasUser.setId(newAliasUserId); + createdAliasUser.setZoneId(customZoneId); + createdAliasUser.setAliasId(originalUserId); + createdAliasUser.setAliasZid(UAA); + when(scimUserProvisioning.createUser( + argThat(new EntityWithAliasMatcher<>(customZoneId, null, originalUserId, UAA)), + eq(UaaStringUtils.EMPTY_STRING), + eq(customZoneId) + )).thenReturn(createdAliasUser); + + // mock update of original user + when(scimUserProvisioning.update( + eq(originalUserId), + argThat(new EntityWithAliasMatcher<>(UAA, originalUserId, newAliasUserId, customZoneId)), + eq(UAA) + )).then(invocationOnMock -> invocationOnMock.getArgument(1)); + + // check if the original user now references the new alias + final ScimUser result = aliasHandler.ensureConsistencyOfAliasEntity( + requestBody, + existingUser + ); + assertThat(result.getAliasId()).isEqualTo(newAliasUserId); + assertThat(result.getAliasZid()).isEqualTo(customZoneId); + + // should update original user with new aliasId + final ArgumentCaptor originalUserCaptor = ArgumentCaptor.forClass(ScimUser.class); + verify(scimUserProvisioning).update(eq(originalUserId), originalUserCaptor.capture(), eq(UAA)); + final ScimUser capturedOriginalUser = originalUserCaptor.getValue(); + assertThat(capturedOriginalUser.getAliasId()).isEqualTo(newAliasUserId); + } + } + + @Nested + class AliasFeatureDisabled extends ExistingAlias_AliasFeatureDisabled { + // all tests defined in superclass + } + } + + @Nested + class NoExistingAlias { + @Nested + class AliasFeatureEnabled extends NoExistingAlias_AliasFeatureEnabled { + // all tests defined in superclass + } + + @Nested + class AliasFeatureDisabled extends NoExistingAlias_AliasFeatureDisabled { + // all tests defined in superclass + } + } +} From 96af3517dd943fa039b94732a78a66e0f390f7c5 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 11 Apr 2024 17:29:46 +0200 Subject: [PATCH 17/25] Change update behavior for SCIM user aliases: no longer propagate timestamps from original user to alias user --- .../uaa/alias/EntityAliasHandler.java | 7 ++++++ .../IdentityProviderAliasHandler.java | 8 ++++++ .../uaa/scim/ScimUserAliasHandler.java | 15 ++++++++++- ...UserAliasHandlerEnsureConsistencyTest.java | 25 ++++++++++++++++++- 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandler.java b/server/src/main/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandler.java index 81457a89f88..85870a31b3e 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandler.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandler.java @@ -155,6 +155,7 @@ public final T ensureConsistencyOfAliasEntity( // update the existing alias entity if (existingAliasEntity != null) { setId(aliasEntity, existingAliasEntity.getId()); + setPropertiesFromExistingAliasEntity(aliasEntity, existingAliasEntity); updateEntity(aliasEntity, originalEntity.getAliasZid()); return originalEntity; } @@ -178,6 +179,12 @@ public final T ensureConsistencyOfAliasEntity( return updateEntity(originalEntity, originalEntity.getZoneId()); } + /** + * Set properties from the existing alias entity in the new alias entity before it is updated. Can be used if + * certain properties should differ between the original and the alias entity. + */ + protected abstract void setPropertiesFromExistingAliasEntity(final T newAliasEntity, final T existingAliasEntity); + private T buildAliasEntity(final T originalEntity) { final T aliasEntity = cloneEntity(originalEntity); aliasEntity.setAliasId(originalEntity.getId()); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandler.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandler.java index c3987b36397..881b0591f82 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandler.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandler.java @@ -45,6 +45,14 @@ protected boolean additionalValidationChecksForNewAlias(@NonNull final IdentityP return IDP_TYPES_ALIAS_SUPPORTED.contains(requestBody.getType()); } + @Override + protected void setPropertiesFromExistingAliasEntity( + final IdentityProvider newAliasEntity, + final IdentityProvider existingAliasEntity + ) { + // not required for identity providers + } + @Override protected void setId(final IdentityProvider entity, final String id) { entity.setId(id); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java index 7c560ea84eb..864f21213a9 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java @@ -53,6 +53,17 @@ protected boolean additionalValidationChecksForNewAlias(final ScimUser requestBo return EntityAliasHandler.isValidAliasPair(idpInCurrentZone.get(), idpInAliasZone.get()); } + @Override + protected void setPropertiesFromExistingAliasEntity( + final ScimUser newAliasEntity, + final ScimUser existingAliasEntity + ) { + // these three timestamps should not be overwritten by the timestamps of the original user + newAliasEntity.setPasswordLastModified(existingAliasEntity.getPasswordLastModified()); + newAliasEntity.setLastLogonTime(existingAliasEntity.getLastLogonTime()); + newAliasEntity.setPreviousLogonTime(existingAliasEntity.getPreviousLogonTime()); + } + private Optional> retrieveIdpByOrigin(final String originKey, final String zoneId) { final IdentityProvider idpInAliasZone; try { @@ -98,7 +109,9 @@ protected ScimUser cloneEntity(final ScimUser originalEntity) { aliasUser.setAliasId(null); aliasUser.setAliasZid(null); - // these timestamps will be overwritten during creation + /* these timestamps will be overwritten: + * - creation: with current timestamp during persistence (JdbcScimUserProvisioning) + * - update: with values from existing alias entity */ aliasUser.setPasswordLastModified(null); aliasUser.setLastLogonTime(null); aliasUser.setPreviousLogonTime(null); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerEnsureConsistencyTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerEnsureConsistencyTest.java index cc014eb79e8..30ead6e2af2 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerEnsureConsistencyTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerEnsureConsistencyTest.java @@ -9,7 +9,9 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.sql.Timestamp; import java.util.Collections; +import java.util.Date; import java.util.Objects; import java.util.UUID; @@ -100,7 +102,10 @@ protected boolean entitiesAreEqual(final ScimUser entity1, final ScimUser entity && Objects.equals(entity1.getAliasZid(), entity2.getAliasZid()) && Objects.equals(entity1.getZoneId(), entity2.getZoneId()) && Objects.equals(entity1.getPassword(), entity2.getPassword()) - && Objects.equals(entity1.getSalt(), entity2.getSalt()); + && Objects.equals(entity1.getSalt(), entity2.getSalt()) + && Objects.equals(entity1.getPreviousLogonTime(), entity2.getPreviousLogonTime()) + && Objects.equals(entity1.getLastLogonTime(), entity2.getLastLogonTime()) + && Objects.equals(entity1.getPasswordLastModified(), entity2.getPasswordLastModified()); } @Override @@ -148,6 +153,12 @@ void shouldPropagateChangesToExistingAlias() { final String aliasId = UUID.randomUUID().toString(); final ScimUser existingUser = buildEntityWithAliasProperties(aliasId, customZoneId); + // set timestamps of original user + final Timestamp timestampOriginalUser = new Timestamp(new Date().getTime()); + existingUser.setPasswordLastModified(timestampOriginalUser); + existingUser.setPreviousLogonTime(timestampOriginalUser.getTime()); + existingUser.setLastLogonTime(timestampOriginalUser.getTime()); + final ScimUser originalUser = shallowCloneEntity(existingUser); final String newGivenName = "some-new-name"; originalUser.setName(new ScimUser.Name(newGivenName, originalUser.getFamilyName())); @@ -158,6 +169,13 @@ void shouldPropagateChangesToExistingAlias() { aliasUser.setZoneId(existingUser.getAliasZid()); aliasUser.setAliasId(existingUser.getId()); aliasUser.setAliasZid(existingUser.getZoneId()); + + // set timestamps of alias user to a different value + final Timestamp timestampAliasUser = new Timestamp(new Date().getTime() + 5000); + aliasUser.setPasswordLastModified(timestampAliasUser); + aliasUser.setPreviousLogonTime(timestampAliasUser.getTime()); + aliasUser.setLastLogonTime(timestampAliasUser.getTime()); + arrangeEntityExists(aliasUser.getId(), aliasUser.getZoneId(), aliasUser); final ScimUser result = aliasHandler.ensureConsistencyOfAliasEntity( @@ -175,6 +193,11 @@ void shouldPropagateChangesToExistingAlias() { assertThat(capturedUser.getId()).isEqualTo(aliasId); assertThat(capturedUser.getZoneId()).isEqualTo(customZoneId); assertThat(capturedUser.getGivenName()).isEqualTo(newGivenName); + + // check if the alias timestamps were left unchanged even though the original user has different ones + assertThat(capturedUser.getPasswordLastModified()).isNotNull().isEqualTo(timestampAliasUser); + assertThat(capturedUser.getPreviousLogonTime()).isNotNull().isEqualTo(timestampAliasUser.getTime()); + assertThat(capturedUser.getLastLogonTime()).isNotNull().isEqualTo(timestampAliasUser.getTime()); } @Test From e78e1fda118099af23976d7df8dccf31e39bee63 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 8 May 2024 13:55:12 +0200 Subject: [PATCH 18/25] Add unit test: creation of alias user fails as username already occupied in the alias zone --- ...UserAliasHandlerEnsureConsistencyTest.java | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerEnsureConsistencyTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerEnsureConsistencyTest.java index 30ead6e2af2..c4ef74f409d 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerEnsureConsistencyTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerEnsureConsistencyTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; @@ -15,9 +16,11 @@ import java.util.Objects; import java.util.UUID; +import org.cloudfoundry.identity.uaa.alias.EntityAliasFailedException; import org.cloudfoundry.identity.uaa.alias.EntityAliasHandler; import org.cloudfoundry.identity.uaa.alias.EntityAliasHandlerEnsureConsistencyTest; import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; +import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceAlreadyExistsException; import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceNotFoundException; import org.cloudfoundry.identity.uaa.util.UaaStringUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; @@ -29,6 +32,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; @ExtendWith(MockitoExtension.class) class ScimUserAliasHandlerEnsureConsistencyTest extends EntityAliasHandlerEnsureConsistencyTest { @@ -134,6 +138,14 @@ protected void mockCreateEntity(final String newId, final String zoneId) { }); } + private void mockCreateEntityThrows_UsernameAlreadyOccupied(final String username, final String zoneId) { + when(scimUserProvisioning.createUser( + argThat(scimUser -> Objects.equals(username, scimUser.getUserName())), + anyString(), + eq(zoneId) + )).thenThrow(new ScimResourceAlreadyExistsException("username already occupied")); + } + @Override protected void arrangeEntityExists(final String id, final String zoneId, final ScimUser entity) { when(scimUserProvisioning.retrieve(id, zoneId)).thenReturn(entity); @@ -259,7 +271,23 @@ class AliasFeatureDisabled extends ExistingAlias_AliasFeatureDisabled { class NoExistingAlias { @Nested class AliasFeatureEnabled extends NoExistingAlias_AliasFeatureEnabled { - // all tests defined in superclass + @Test + void shouldThrow_WhenUsernameAlreadyOccupiedInAliasZone() { + final ScimUser existingEntity = buildEntityWithAliasProperties(null, null); + final ScimUser originalEntity = shallowCloneEntity(existingEntity); + originalEntity.setAliasZid(customZoneId); + + mockCreateEntityThrows_UsernameAlreadyOccupied(originalEntity.getUserName(), customZoneId); + + final EntityAliasFailedException exception = assertThrows(EntityAliasFailedException.class, () -> + aliasHandler.ensureConsistencyOfAliasEntity(originalEntity, existingEntity) + ); + assertThat(exception.getHttpStatus()).isEqualTo(HttpStatus.CONFLICT.value()); + assertThat(exception.getMessage()).isEqualTo( + "Could not create ScimUser[id=null,zid='%s',aliasId='%s',aliasZid='uaa']. A user with the same username already exists in the alias zone." + .formatted(customZoneId, existingEntity.getId()) + ); + } } @Nested From 5699f4305c4bc9aaf1219b72ee139414c1de2f8d Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 3 Jun 2024 11:14:26 +0200 Subject: [PATCH 19/25] Remove setting properties of new alias explicitly to null in ScimAliasHandler --- .../uaa/scim/ScimUserAliasHandler.java | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java index 864f21213a9..93322266cb0 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java @@ -88,7 +88,6 @@ protected void setZoneId(final ScimUser entity, final String zoneId) { protected ScimUser cloneEntity(final ScimUser originalEntity) { final ScimUser aliasUser = new ScimUser(); - aliasUser.setId(null); aliasUser.setExternalId(originalEntity.getExternalId()); /* we only allow alias users to be created if their origin IdP has an alias to the same zone, therefore, an IdP @@ -104,18 +103,6 @@ protected ScimUser cloneEntity(final ScimUser originalEntity) { aliasUser.setActive(originalEntity.isActive()); aliasUser.setVerified(originalEntity.isVerified()); - // idzId and alias properties will be set later - aliasUser.setZoneId(null); - aliasUser.setAliasId(null); - aliasUser.setAliasZid(null); - - /* these timestamps will be overwritten: - * - creation: with current timestamp during persistence (JdbcScimUserProvisioning) - * - update: with values from existing alias entity */ - aliasUser.setPasswordLastModified(null); - aliasUser.setLastLogonTime(null); - aliasUser.setPreviousLogonTime(null); - /* password: empty string * - alias users are only allowed for IdPs that also have an alias * - IdPs can only have an alias if they are of type SAML, OIDC or OAuth 2.0 @@ -124,6 +111,14 @@ protected ScimUser cloneEntity(final ScimUser originalEntity) { aliasUser.setPassword(EMPTY_STRING); aliasUser.setSalt(null); + /* The following fields will be overwritten later and are therefore not set here: + * - id and identityZoneId + * - aliasId and aliasZid + * - timestamp fields (password last modified, last logon, previous logon): + * - creation: with current timestamp during persistence (JdbcScimUserProvisioning) + * - update: with values from existing alias entity + */ + return aliasUser; } From 0cf1ba70f349141d99ee566efa0a4a972a7c18b5 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 3 Jun 2024 14:18:10 +0200 Subject: [PATCH 20/25] Refactor --- .../identity/uaa/scim/ScimUserAliasHandlerValidationTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java index a0dd39139cf..de3d1aa2571 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java @@ -247,8 +247,8 @@ private static IdentityProvider buildIdp( return idp; } - private void arrangeIdpDoesNotExist(final String origin, final String aliasZid) { - when(identityProviderProvisioning.retrieveByOrigin(origin, aliasZid)) + private void arrangeIdpDoesNotExist(final String origin, final String zoneId) { + when(identityProviderProvisioning.retrieveByOrigin(origin, zoneId)) .thenThrow(new EmptyResultDataAccessException(1)); } From 97d13b31d85b3f375f7fc8e38fbf9ecebc9798aa Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 3 Jun 2024 14:20:07 +0200 Subject: [PATCH 21/25] Add unit test for SCIM alias validation: no existing alias, alias feature enabled, no IdP exists for original user --- .../ScimUserAliasHandlerValidationTest.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java index de3d1aa2571..431935dab05 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java @@ -140,6 +140,51 @@ private void shouldReturnFalse_IfIdpHasNoAlias( assertThat(aliasHandler.aliasPropertiesAreValid(requestBody, existingUser)).isFalse(); } + @ParameterizedTest + @MethodSource("provideExistingEntityArguments") + void shouldReturnFalse_IfIdpOfOriginalUserDoesNotExist_UaaToCustomZone( + final ExistingEntityArgument existingEntityArgument + ) { + shouldReturnFalse_IfIdpOfOriginalUserDoesNotExist(existingEntityArgument, UAA, customZoneId); + } + + @ParameterizedTest + @MethodSource("provideExistingEntityArguments") + void shouldReturnFalse_IfIdpOfOriginalUserDoesNotExist_CustomToUaaZone( + final ExistingEntityArgument existingEntityArgument + ) { + shouldReturnFalse_IfIdpOfOriginalUserDoesNotExist(existingEntityArgument, customZoneId, UAA); + } + + private void shouldReturnFalse_IfIdpOfOriginalUserDoesNotExist( + final ExistingEntityArgument existingEntityArgument, + final String zone1, + final String zone2 + ) { + arrangeZoneExists(zone1); + arrangeZoneExists(zone2); + + arrangeCurrentIdz(zone1); + + final ScimUser requestBody = buildEntityWithAliasProps(null, zone2); + requestBody.setZoneId(zone1); + final String origin = RANDOM_STRING_GENERATOR.generate(); + requestBody.setOrigin(origin); + + final ScimUser existingUser = resolveExistingEntityArgument(existingEntityArgument); + if (existingUser != null) { + existingUser.setZoneId(zone1); + existingUser.setOrigin(origin); + } + + // arrange IdP exists for alias user, but not for original user + final IdentityProvider idp = buildIdp(UUID.randomUUID().toString(), origin, zone1, null, null); + arrangeIdpDoesNotExist(origin, zone1); + arrangeIdpExists(origin, zone2, idp); + + assertThat(aliasHandler.aliasPropertiesAreValid(requestBody, existingUser)).isFalse(); + } + @ParameterizedTest @MethodSource("provideExistingEntityArguments") void shouldReturnFalse_IfIdpHasAliasToDifferentZoneThanUser( From 4321693cca3862e77789d58fa64b58676119946d Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 3 Jun 2024 14:31:20 +0200 Subject: [PATCH 22/25] Add unit test for SCIM alias validation: no existing alias, alias feature enabled, IdP of original user has empty aliasId --- .../ScimUserAliasHandlerValidationTest.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java index 431935dab05..a13164e8b3e 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java @@ -223,6 +223,55 @@ void shouldReturnFalse_IfIdpHasAliasToDifferentZoneThanUser( assertThat(aliasHandler.aliasPropertiesAreValid(requestBody, existingUser)).isFalse(); } + @ParameterizedTest + @MethodSource("provideExistingEntityArguments") + void shouldReturnFalse_IfIdpOfOriginalUserHasEmptyAliasId_UaaToCustomZone( + final ExistingEntityArgument existingEntityArgument + ) { + shouldReturnFalse_IfIdpOfOriginalUserHasEmptyAliasId(existingEntityArgument, UAA, customZoneId); + } + + @ParameterizedTest + @MethodSource("provideExistingEntityArguments") + void shouldReturnFalse_IfIdpOfOriginalUserHasEmptyAliasId_CustomToUaaZone( + final ExistingEntityArgument existingEntityArgument + ) { + shouldReturnFalse_IfIdpOfOriginalUserHasEmptyAliasId(existingEntityArgument, customZoneId, UAA); + } + + private void shouldReturnFalse_IfIdpOfOriginalUserHasEmptyAliasId( + final ExistingEntityArgument existingEntityArgument, + final String zone1, + final String zone2 + ) { + arrangeZoneExists(zone1); + arrangeZoneExists(zone2); + + arrangeCurrentIdz(zone1); + + final ScimUser requestBody = buildEntityWithAliasProps(null, zone2); + requestBody.setZoneId(zone1); + final String origin = RANDOM_STRING_GENERATOR.generate(); + requestBody.setOrigin(origin); + + final ScimUser existingUser = resolveExistingEntityArgument(existingEntityArgument); + if (existingUser != null) { + existingUser.setZoneId(zone1); + existingUser.setOrigin(origin); + } + + // arrange IdP exists with alias in same zone as the one referenced in the user, but with empty aliasId + final String idpId = UUID.randomUUID().toString(); + final String aliasIdpId = UUID.randomUUID().toString(); + final IdentityProvider idp = buildIdp(idpId, origin, zone1, aliasIdpId, zone2); + idp.setAliasId(""); // arrange IdP of original user has empty alias ID + final IdentityProvider aliasIdp = buildIdp(aliasIdpId, origin, zone2, idpId, zone1); + arrangeIdpExists(origin, zone1, idp); + arrangeIdpExists(origin, zone2, aliasIdp); + + assertThat(aliasHandler.aliasPropertiesAreValid(requestBody, existingUser)).isFalse(); + } + @ParameterizedTest @MethodSource("provideExistingEntityArguments") void shouldReturnTrue_IfIdpHasAliasToSameZoneAsUser_UaaToCustomZone( From 31ebf13fd0ed11dc08e081a9f7dddcd7cde325e8 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 3 Jun 2024 14:44:56 +0200 Subject: [PATCH 23/25] Add missing empty line --- .../identity/uaa/scim/ScimUserAliasHandlerValidationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java index a13164e8b3e..29da24b5338 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandlerValidationTest.java @@ -377,4 +377,4 @@ class AliasFeatureDisabled extends ExistingAlias_AliasFeatureDisabled { // all tests defined in superclass } } -} \ No newline at end of file +} From 823fc2fbfabe24a183b3d348443efdf305969dc8 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 3 Jun 2024 16:46:14 +0200 Subject: [PATCH 24/25] Introduce bean for aliasEntitiesEnabled configuration property --- .../identity/uaa/alias/AliasEntitiesConfig.java | 13 +++++++++++++ .../uaa/provider/IdentityProviderAliasHandler.java | 2 +- .../uaa/provider/IdentityProviderEndpoints.java | 2 +- .../identity/uaa/scim/ScimUserAliasHandler.java | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/alias/AliasEntitiesConfig.java diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/alias/AliasEntitiesConfig.java b/server/src/main/java/org/cloudfoundry/identity/uaa/alias/AliasEntitiesConfig.java new file mode 100644 index 00000000000..d7d5e8bc999 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/alias/AliasEntitiesConfig.java @@ -0,0 +1,13 @@ +package org.cloudfoundry.identity.uaa.alias; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AliasEntitiesConfig { + @Bean + public boolean aliasEntitiesEnabled(@Value("${login.aliasEntitiesEnabled:false}") final boolean enabled) { + return enabled; + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandler.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandler.java index 881b0591f82..1c64f0dd1e6 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandler.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandler.java @@ -33,7 +33,7 @@ public class IdentityProviderAliasHandler extends EntityAliasHandler Date: Mon, 3 Jun 2024 17:26:49 +0200 Subject: [PATCH 25/25] Remove unused imports --- .../identity/uaa/provider/IdentityProviderAliasHandler.java | 1 - .../identity/uaa/provider/IdentityProviderEndpoints.java | 1 - .../org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java | 1 - 3 files changed, 3 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandler.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandler.java index 1c64f0dd1e6..3fe5a142323 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandler.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderAliasHandler.java @@ -13,7 +13,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.http.HttpStatus; import org.springframework.lang.NonNull; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpoints.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpoints.java index cca4a126053..1fd9f799ea1 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpoints.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpoints.java @@ -53,7 +53,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.dao.EmptyResultDataAccessException; diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java index 327eaa5439e..307faf27314 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/ScimUserAliasHandler.java @@ -13,7 +13,6 @@ import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component;