From 4c05f309ee8d3d494aaef51db9a76f28cf526efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20H=C3=B6lzl?= <102049638+adrianhoelzl-sap@users.noreply.github.com> Date: Wed, 4 Sep 2024 17:10:27 +0200 Subject: [PATCH] Fix Update of Users with Alias during Login (#3014) * Add ScimUserService as central entrypoint for ScimUser update with alias handling * Move ScimUser update logic from ScimUserEndpoints to ScimUserService * Add injection of ScimUserService into ScimUserBootstrap * Reformat * Add handling of alias users in ScimUserBootstrap * Fix ScimUserEndpointsAliasMockMvcTests * Add test for new behavior to ScimUserBootstrapTests * Add unit test for ScimUserService.update * Add documentation for alias user handling during logon * Inject 'aliasEntitiesEnabled into ScimUserBootstrap' * Handle login of users with alias when alias entities are enabled: only update original user while alias properties remain unchanged * Update documentation * Sonar --- docs/UAA-Alias-Entities.md | 10 +- .../AliasPropertiesInvalidException.java | 7 + .../uaa/scim/ScimUserAliasHandler.java | 2 +- .../uaa/scim/bootstrap/ScimUserBootstrap.java | 40 ++- .../uaa/scim/endpoints/ScimUserEndpoints.java | 37 +- .../uaa/scim/services/ScimUserService.java | 71 ++++ .../LoginSamlAuthenticationProviderTests.java | 23 +- .../bootstrap/ScimUserBootstrapTests.java | 338 ++++++++++++++++-- .../ScimUserEndpointsAliasTests.java | 10 + .../scim/services/ScimUserServiceTest.java | 222 ++++++++++++ .../ScimUserEndpointsAliasMockMvcTests.java | 4 + .../endpoints/ScimUserEndpointsTests.java | 14 +- 12 files changed, 699 insertions(+), 79 deletions(-) create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/alias/AliasPropertiesInvalidException.java create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/scim/services/ScimUserService.java create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/scim/services/ScimUserServiceTest.java diff --git a/docs/UAA-Alias-Entities.md b/docs/UAA-Alias-Entities.md index 4d1d8ae70ca..f5fdc27da79 100644 --- a/docs/UAA-Alias-Entities.md +++ b/docs/UAA-Alias-Entities.md @@ -115,9 +115,17 @@ Please note that disabling the flag does not lead to existing entities with alia In addition to enabling the alias feature, one must ensure that no groups can be created that would give users inside a custom zone any authorizations in other zones (e.g., `zones..admin`). This can be achieved by using the allow list for groups (`userConfig.allowedGroups`) in the configuration of the -identity zone. +identity zone. +## User Logon +During logon, the information of the matching shadow user is updated with the information from the identity provider +(e.g., the ID token in the OpenID Connect flow). + +If this shadow user has an alias, ... +- *alias entities enabled:* the updated properties are propagated to the alias. +- *alias entities disabled:* only the user itself is updated, the alias user is left unchanged. + - the alias properties are not changed - original and alias user still reference each other diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/alias/AliasPropertiesInvalidException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/alias/AliasPropertiesInvalidException.java new file mode 100644 index 00000000000..8490a960ca3 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/alias/AliasPropertiesInvalidException.java @@ -0,0 +1,7 @@ +package org.cloudfoundry.identity.uaa.alias; + +public class AliasPropertiesInvalidException extends RuntimeException { + public AliasPropertiesInvalidException() { + super("The fields 'aliasId' and/or 'aliasZid' are invalid."); + } +} 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 48b89b50016..e3a3c1c1349 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 @@ -23,7 +23,7 @@ public class ScimUserAliasHandler extends EntityAliasHandler { private final IdentityProviderProvisioning identityProviderProvisioning; private final IdentityZoneManager identityZoneManager; - protected ScimUserAliasHandler( + public ScimUserAliasHandler( @Qualifier("identityZoneProvisioning") final IdentityZoneProvisioning identityZoneProvisioning, final ScimUserProvisioning scimUserProvisioning, final IdentityProviderProvisioning identityProviderProvisioning, diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java index 01d8dbb8e87..d4c557ceb43 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrap.java @@ -12,11 +12,13 @@ import org.cloudfoundry.identity.uaa.scim.exception.MemberAlreadyExistsException; import org.cloudfoundry.identity.uaa.scim.exception.MemberNotFoundException; import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceNotFoundException; +import org.cloudfoundry.identity.uaa.scim.services.ScimUserService; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; @@ -49,11 +51,13 @@ public class ScimUserBootstrap implements private static final Logger logger = LoggerFactory.getLogger(ScimUserBootstrap.class); private final ScimUserProvisioning scimUserProvisioning; + private final ScimUserService scimUserService; private final ScimGroupProvisioning scimGroupProvisioning; private final ScimGroupMembershipManager membershipManager; private final Collection users; private final boolean override; private final List usersToDelete; + private final boolean aliasEntitiesEnabled; private ApplicationEventPublisher publisher; /** @@ -61,18 +65,24 @@ public class ScimUserBootstrap implements * @param users Users to create * @param override Flag to indicate that user accounts can be updated as well as created */ - public ScimUserBootstrap(final ScimUserProvisioning scimUserProvisioning, - final ScimGroupProvisioning scimGroupProvisioning, - final ScimGroupMembershipManager membershipManager, - final Collection users, - @Value("${scim.user.override:false}") final boolean override, - @Value("${delete.users:#{null}}") final List usersToDelete) { + public ScimUserBootstrap( + final ScimUserProvisioning scimUserProvisioning, + final ScimUserService scimUserService, + final ScimGroupProvisioning scimGroupProvisioning, + final ScimGroupMembershipManager membershipManager, + final Collection users, + @Value("${scim.user.override:false}") final boolean override, + @Value("${delete.users:#{null}}") final List usersToDelete, + @Qualifier("aliasEntitiesEnabled") final boolean aliasEntitiesEnabled + ) { this.scimUserProvisioning = scimUserProvisioning; + this.scimUserService = scimUserService; this.scimGroupProvisioning = scimGroupProvisioning; this.membershipManager = membershipManager; this.users = Collections.unmodifiableCollection(users); this.override = override; this.usersToDelete = usersToDelete; + this.aliasEntitiesEnabled = aliasEntitiesEnabled; } @Override @@ -160,7 +170,23 @@ private void updateUser(ScimUser existingUser, UaaUser updatedUser, boolean upda final ScimUser newScimUser = convertToScimUser(updatedUser); newScimUser.setVersion(existingUser.getVersion()); - scimUserProvisioning.update(id, newScimUser, IdentityZoneHolder.get().getId()); + newScimUser.setZoneId(existingUser.getZoneId()); + + /* the user in the event won't have the alias properties set, we must therefore propagate them from the existing + * user, if present */ + if (hasText(existingUser.getAliasId()) && hasText(existingUser.getAliasZid())) { + newScimUser.setAliasId(existingUser.getAliasId()); + newScimUser.setAliasZid(existingUser.getAliasZid()); + } + + if (aliasEntitiesEnabled) { + // update the user and propagate the changes to the alias, if present + scimUserService.updateUser(id, newScimUser); + } else { + // update only the original user, even if it has an alias (the alias properties remain unchanged) + scimUserProvisioning.update(id, newScimUser, IdentityZoneHolder.get().getId()); + } + if (OriginKeys.UAA.equals(newScimUser.getOrigin()) && hasText(updatedUser.getPassword())) { //password is not relevant for non UAA users scimUserProvisioning.changePassword(id, null, updatedUser.getPassword(), IdentityZoneHolder.get().getId()); } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpoints.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpoints.java index f3cf830994b..ad2bb41a92b 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpoints.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpoints.java @@ -3,6 +3,7 @@ import com.jayway.jsonpath.JsonPathException; import org.cloudfoundry.identity.uaa.account.UserAccountStatus; import org.cloudfoundry.identity.uaa.account.event.UserAccountUnlockedEvent; +import org.cloudfoundry.identity.uaa.alias.AliasPropertiesInvalidException; import org.cloudfoundry.identity.uaa.alias.EntityAliasFailedException; import org.cloudfoundry.identity.uaa.approval.Approval; import org.cloudfoundry.identity.uaa.approval.ApprovalStore; @@ -33,6 +34,7 @@ import org.cloudfoundry.identity.uaa.scim.exception.ScimException; import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceConflictException; import org.cloudfoundry.identity.uaa.scim.exception.UserAlreadyVerifiedException; +import org.cloudfoundry.identity.uaa.scim.services.ScimUserService; import org.cloudfoundry.identity.uaa.scim.util.ScimUtils; import org.cloudfoundry.identity.uaa.scim.validate.PasswordValidator; import org.cloudfoundry.identity.uaa.security.IsSelfCheck; @@ -142,6 +144,7 @@ public class ScimUserEndpoints implements InitializingBean, ApplicationEventPubl */ private final AtomicInteger scimDeletes; private final Map errorCounts; + private final ScimUserService scimUserService; private final ScimUserAliasHandler aliasHandler; private final TransactionTemplate transactionTemplate; @@ -161,6 +164,7 @@ public ScimUserEndpoints( final ExpiringCodeStore codeStore, final ApprovalStore approvalStore, final ScimGroupMembershipManager membershipManager, + final ScimUserService scimUserService, final ScimUserAliasHandler aliasHandler, final TransactionTemplate transactionTemplate, final @Qualifier("aliasEntitiesEnabled") boolean aliasEntitiesEnabled, @@ -187,6 +191,7 @@ public ScimUserEndpoints( this.messageConverters = new HttpMessageConverter[] { new ExceptionReportHttpMessageConverter() }; + this.scimUserService = scimUserService; this.aliasHandler = aliasHandler; this.transactionTemplate = transactionTemplate; scimUpdates = new AtomicInteger(); @@ -319,23 +324,11 @@ public ScimUser updateUser(@RequestBody ScimUser user, @PathVariable String user user.setZoneId(identityZoneManager.getCurrentIdentityZoneId()); - final ScimUser existingScimUser = scimUserProvisioning.retrieve( - userId, - identityZoneManager.getCurrentIdentityZoneId() - ); - if (!aliasHandler.aliasPropertiesAreValid(user, existingScimUser)) { - throw new ScimException("The fields 'aliasId' and/or 'aliasZid' are invalid.", HttpStatus.BAD_REQUEST); - } - final ScimUser scimUser; try { - if (aliasEntitiesEnabled) { - // update user and create/update alias, if necessary - scimUser = updateUserWithAliasHandling(userId, user, existingScimUser); - } else { - // update user without alias handling - scimUser = scimUserProvisioning.update(userId, user, identityZoneManager.getCurrentIdentityZoneId()); - } + scimUser = scimUserService.updateUser(userId, user); + } catch (final AliasPropertiesInvalidException e) { + throw new ScimException("The fields 'aliasId' and/or 'aliasZid' are invalid.", HttpStatus.BAD_REQUEST); } catch (final OptimisticLockingFailureException e) { throw new ScimResourceConflictException(e.getMessage()); } catch (final EntityAliasFailedException e) { @@ -348,20 +341,6 @@ public ScimUser updateUser(@RequestBody ScimUser user, @PathVariable String user return scimUserWithApprovalsAndGroups; } - private ScimUser updateUserWithAliasHandling(final String userId, final ScimUser user, final ScimUser existingUser) { - return transactionTemplate.execute(txStatus -> { - final ScimUser updatedOriginalUser = scimUserProvisioning.update( - userId, - user, - identityZoneManager.getCurrentIdentityZoneId() - ); - return aliasHandler.ensureConsistencyOfAliasEntity( - updatedOriginalUser, - existingUser - ); - }); - } - @RequestMapping(value = "/Users/{userId}", method = RequestMethod.PATCH) @ResponseBody public ScimUser patchUser(@RequestBody ScimUser patch, @PathVariable String userId, diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/scim/services/ScimUserService.java b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/services/ScimUserService.java new file mode 100644 index 00000000000..d0e36fb16ac --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/scim/services/ScimUserService.java @@ -0,0 +1,71 @@ +package org.cloudfoundry.identity.uaa.scim.services; + +import org.cloudfoundry.identity.uaa.alias.AliasPropertiesInvalidException; +import org.cloudfoundry.identity.uaa.alias.EntityAliasFailedException; +import org.cloudfoundry.identity.uaa.scim.ScimUser; +import org.cloudfoundry.identity.uaa.scim.ScimUserAliasHandler; +import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; +import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.stereotype.Component; +import org.springframework.transaction.support.TransactionTemplate; + +@Component +public class ScimUserService { + + private final ScimUserAliasHandler aliasHandler; + private final ScimUserProvisioning scimUserProvisioning; + private final IdentityZoneManager identityZoneManager; + private final TransactionTemplate transactionTemplate; + private final boolean aliasEntitiesEnabled; + + public ScimUserService( + final ScimUserAliasHandler aliasHandler, + final ScimUserProvisioning scimUserProvisioning, + final IdentityZoneManager identityZoneManager, + final TransactionTemplate transactionTemplate, + @Qualifier("aliasEntitiesEnabled") final boolean aliasEntitiesEnabled + ) { + this.aliasHandler = aliasHandler; + this.scimUserProvisioning = scimUserProvisioning; + this.identityZoneManager = identityZoneManager; + this.transactionTemplate = transactionTemplate; + this.aliasEntitiesEnabled = aliasEntitiesEnabled; + } + + public ScimUser updateUser(final String userId, final ScimUser user) + throws AliasPropertiesInvalidException, OptimisticLockingFailureException, EntityAliasFailedException { + final ScimUser existingScimUser = scimUserProvisioning.retrieve( + userId, + identityZoneManager.getCurrentIdentityZoneId() + ); + if (!aliasHandler.aliasPropertiesAreValid(user, existingScimUser)) { + throw new AliasPropertiesInvalidException(); + } + + if (!aliasEntitiesEnabled) { + // update user without alias handling + return scimUserProvisioning.update(userId, user, identityZoneManager.getCurrentIdentityZoneId()); + } + + // update user and create/update alias, if necessary + return updateUserWithAliasHandling(userId, user, existingScimUser); + } + + private ScimUser updateUserWithAliasHandling( + final String userId, + final ScimUser user, + final ScimUser existingUser + ) { + return transactionTemplate.execute(txStatus -> { + final ScimUser updatedOriginalUser = scimUserProvisioning.update( + userId, + user, + identityZoneManager.getCurrentIdentityZoneId() + ); + return aliasHandler.ensureConsistencyOfAliasEntity(updatedOriginalUser, existingUser); + }); + } + +} diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/LoginSamlAuthenticationProviderTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/LoginSamlAuthenticationProviderTests.java index ef7bdafb2d0..bf7c108396c 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/LoginSamlAuthenticationProviderTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/saml/LoginSamlAuthenticationProviderTests.java @@ -16,12 +16,14 @@ import org.cloudfoundry.identity.uaa.scim.ScimGroup; import org.cloudfoundry.identity.uaa.scim.ScimGroupProvisioning; import org.cloudfoundry.identity.uaa.scim.ScimUser; +import org.cloudfoundry.identity.uaa.scim.ScimUserAliasHandler; import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; import org.cloudfoundry.identity.uaa.scim.bootstrap.ScimUserBootstrap; import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimGroupExternalMembershipManager; import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimGroupMembershipManager; import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimGroupProvisioning; import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimUserProvisioning; +import org.cloudfoundry.identity.uaa.scim.services.ScimUserService; import org.cloudfoundry.identity.uaa.test.TestUtils; import org.cloudfoundry.identity.uaa.user.JdbcUaaUserDatabase; import org.cloudfoundry.identity.uaa.user.UaaAuthority; @@ -200,7 +202,26 @@ void configureProvider() throws SAMLException, SecurityException, DecryptionExce JdbcScimGroupMembershipManager membershipManager = new JdbcScimGroupMembershipManager( jdbcTemplate, new TimeServiceImpl(), userProvisioning, null, dbUtils); membershipManager.setScimGroupProvisioning(groupProvisioning); - ScimUserBootstrap bootstrap = new ScimUserBootstrap(userProvisioning, groupProvisioning, membershipManager, Collections.emptyList(), false, Collections.emptyList()); + + final ScimUserAliasHandler aliasHandler = mock(ScimUserAliasHandler.class); + when(aliasHandler.aliasPropertiesAreValid(any(), any())).thenReturn(true); + + ScimUserBootstrap bootstrap = new ScimUserBootstrap( + userProvisioning, + new ScimUserService( + aliasHandler, + userProvisioning, + identityZoneManager, + null, // not required since alias is disabled + false + ), + groupProvisioning, + membershipManager, + Collections.emptyList(), + false, + Collections.emptyList(), + false + ); externalManager = new JdbcScimGroupExternalMembershipManager(jdbcTemplate, dbUtils); externalManager.setScimGroupProvisioning(groupProvisioning); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrapTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrapTests.java index c2494821483..0cc637ef5c2 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrapTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/bootstrap/ScimUserBootstrapTests.java @@ -1,29 +1,38 @@ package org.cloudfoundry.identity.uaa.scim.bootstrap; +import org.apache.commons.lang3.tuple.Triple; import org.cloudfoundry.identity.uaa.annotations.WithDatabaseContext; import org.cloudfoundry.identity.uaa.audit.event.EntityDeletedEvent; import org.cloudfoundry.identity.uaa.authentication.manager.ExternalGroupAuthorizationEvent; import org.cloudfoundry.identity.uaa.authentication.manager.InvitedUserAuthenticatedEvent; import org.cloudfoundry.identity.uaa.constants.OriginKeys; +import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; +import org.cloudfoundry.identity.uaa.provider.JdbcIdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.resources.jdbc.JdbcPagingListFactory; import org.cloudfoundry.identity.uaa.resources.jdbc.LimitSqlAdapterFactory; import org.cloudfoundry.identity.uaa.scim.ScimGroup; import org.cloudfoundry.identity.uaa.scim.ScimGroupMember; import org.cloudfoundry.identity.uaa.scim.ScimUser; +import org.cloudfoundry.identity.uaa.scim.ScimUserAliasHandler; import org.cloudfoundry.identity.uaa.scim.endpoints.ScimUserEndpoints; import org.cloudfoundry.identity.uaa.scim.exception.InvalidPasswordException; import org.cloudfoundry.identity.uaa.scim.exception.MemberNotFoundException; import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimGroupMembershipManager; import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimGroupProvisioning; import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimUserProvisioning; +import org.cloudfoundry.identity.uaa.scim.services.ScimUserService; import org.cloudfoundry.identity.uaa.security.IsSelfCheck; import org.cloudfoundry.identity.uaa.test.TestUtils; +import org.cloudfoundry.identity.uaa.user.UaaAuthority; import org.cloudfoundry.identity.uaa.user.UaaUser; +import org.cloudfoundry.identity.uaa.user.UaaUserPrototype; +import org.cloudfoundry.identity.uaa.util.AlphanumericRandomValueStringGenerator; import org.cloudfoundry.identity.uaa.util.beans.DbUtils; import org.cloudfoundry.identity.uaa.util.TimeServiceImpl; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.JdbcIdentityZoneProvisioning; +import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager; import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManagerImpl; import org.hamcrest.collection.IsArrayContainingInAnyOrder; import org.junit.jupiter.api.AfterEach; @@ -43,6 +52,10 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.crypto.password.PasswordEncoder; import org.cloudfoundry.identity.uaa.oauth.common.util.RandomValueStringGenerator; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.util.StringUtils; import java.sql.SQLException; import java.util.ArrayList; @@ -74,6 +87,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @WithDatabaseContext class ScimUserBootstrapTests { @@ -91,18 +105,40 @@ class ScimUserBootstrapTests { @Autowired private PasswordEncoder passwordEncoder; + private ScimUserService scimUserService; + private JdbcIdentityZoneProvisioning identityZoneProvisioning; + private IdentityZoneManager identityZoneManager; + private IdentityProviderProvisioning idpProvisioning; @BeforeEach void init() throws SQLException { JdbcPagingListFactory pagingListFactory = new JdbcPagingListFactory(namedJdbcTemplate, LimitSqlAdapterFactory.getLimitSqlAdapter()); - jdbcScimUserProvisioning = spy(new JdbcScimUserProvisioning(namedJdbcTemplate, pagingListFactory, passwordEncoder, new IdentityZoneManagerImpl(), new JdbcIdentityZoneProvisioning(jdbcTemplate))); + identityZoneManager = new IdentityZoneManagerImpl(); + identityZoneProvisioning = new JdbcIdentityZoneProvisioning(jdbcTemplate); + jdbcScimUserProvisioning = spy(new JdbcScimUserProvisioning(namedJdbcTemplate, pagingListFactory, passwordEncoder, + identityZoneManager, identityZoneProvisioning)); DbUtils dbUtils = new DbUtils(); jdbcScimGroupProvisioning = new JdbcScimGroupProvisioning(namedJdbcTemplate, pagingListFactory, dbUtils); jdbcScimGroupMembershipManager = new JdbcScimGroupMembershipManager( jdbcTemplate, new TimeServiceImpl(), jdbcScimUserProvisioning, null, dbUtils); jdbcScimGroupMembershipManager.setScimGroupProvisioning(jdbcScimGroupProvisioning); + idpProvisioning = new JdbcIdentityProviderProvisioning(jdbcTemplate); + final ScimUserAliasHandler scimUserAliasHandler = new ScimUserAliasHandler( + identityZoneProvisioning, + jdbcScimUserProvisioning, + idpProvisioning, + identityZoneManager, + false + ); + scimUserService = new ScimUserService( + scimUserAliasHandler, + jdbcScimUserProvisioning, + identityZoneManager, + null, // not required since alias is disabled + false + ); scimUserEndpoints = new ScimUserEndpoints( - new IdentityZoneManagerImpl(), + identityZoneManager, new IsSelfCheck(null), jdbcScimUserProvisioning, null, @@ -112,6 +148,7 @@ void init() throws SQLException { null, null, jdbcScimGroupMembershipManager, + scimUserService, null, null, false, @@ -134,9 +171,13 @@ void tearDown(@Autowired ApplicationContext applicationContext) throws SQLExcept @Test void canDeleteUsersButOnlyInDefaultZone() throws Exception { String randomZoneId = "randomZoneId-" + new RandomValueStringGenerator().generate().toLowerCase(); - canAddUsers(OriginKeys.UAA, IdentityZone.getUaaZoneId(), jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager); - canAddUsers(OriginKeys.LDAP, IdentityZone.getUaaZoneId(), jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager); - canAddUsers(OriginKeys.UAA, randomZoneId, jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager); //this is just an update of the same two users, zoneId is ignored + canAddUsers(OriginKeys.UAA, IdentityZone.getUaaZoneId(), jdbcScimUserProvisioning, jdbcScimGroupProvisioning, + jdbcScimGroupMembershipManager, scimUserService); + canAddUsers(OriginKeys.LDAP, IdentityZone.getUaaZoneId(), jdbcScimUserProvisioning, jdbcScimGroupProvisioning, + jdbcScimGroupMembershipManager, scimUserService); + //this is just an update of the same two users, zoneId is ignored + canAddUsers(OriginKeys.UAA, randomZoneId, jdbcScimUserProvisioning, jdbcScimGroupProvisioning, + jdbcScimGroupMembershipManager, scimUserService); List users = jdbcScimUserProvisioning.retrieveAll(IdentityZone.getUaaZoneId()); assertEquals(4, users.size()); reset(jdbcScimUserProvisioning); @@ -149,7 +190,7 @@ void canDeleteUsersButOnlyInDefaultZone() throws Exception { .when(publisher).publishEvent(any(EntityDeletedEvent.class)); List usersToDelete = Arrays.asList("joe", "mabel", "non-existent"); - ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, emptyList(), false, usersToDelete); + ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, emptyList(), false, usersToDelete, false); bootstrap.setApplicationEventPublisher(publisher); bootstrap.afterPropertiesSet(); bootstrap.onApplicationEvent(mock(ContextRefreshedEvent.class)); @@ -166,7 +207,7 @@ void canDeleteUsersButOnlyInDefaultZone() throws Exception { void slatedForDeleteDoesNotAdd() { UaaUser joe = new UaaUser("joe", "password", "joe@test.org", "Joe", "User"); UaaUser mabel = new UaaUser("mabel", "password", "mabel@blah.com", "Mabel", "User"); - ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Arrays.asList(joe, mabel), false, Arrays.asList("joe", "mabel")); + ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Arrays.asList(joe, mabel), false, Arrays.asList("joe", "mabel"), false); bootstrap.afterPropertiesSet(); String zoneId = IdentityZone.getUaaZoneId(); verify(jdbcScimUserProvisioning, never()).create(any(), eq(zoneId)); @@ -176,7 +217,7 @@ void slatedForDeleteDoesNotAdd() { @Test void canAddUsers() throws Exception { - canAddUsers(OriginKeys.UAA, IdentityZone.getUaaZoneId(), jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager); + canAddUsers(OriginKeys.UAA, IdentityZone.getUaaZoneId(), jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, scimUserService); Collection users = jdbcScimUserProvisioning.retrieveAll(IdentityZone.getUaaZoneId()); assertEquals(2, users.size()); } @@ -184,7 +225,7 @@ void canAddUsers() throws Exception { @Test void addedUsersAreVerified() { UaaUser uaaJoe = new UaaUser("joe", "password", "joe@test.org", "Joe", "User"); - ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(uaaJoe), false, Collections.emptyList()); + ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(uaaJoe), false, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); @@ -198,7 +239,7 @@ void addedUsersAreVerified() { void canAddUserWithAuthorities() { UaaUser joe = new UaaUser("joe", "password", "joe@test.org", "Joe", "User"); joe = joe.authorities(AuthorityUtils.commaSeparatedStringToAuthorityList("openid,read")); - ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList()); + ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); @SuppressWarnings("unchecked") Collection> users = (Collection>) scimUserEndpoints.findUsers("id", @@ -215,7 +256,7 @@ void canAddUserWithAuthorities() { void cannotAddUserWithNoPassword() { UaaUser joe = new UaaUser("joe", "", "joe@test.org", "Joe", "User", OriginKeys.UAA, null); joe = joe.authorities(AuthorityUtils.commaSeparatedStringToAuthorityList("openid,read")); - ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList()); + ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList(), false); assertThrows(InvalidPasswordException.class, bootstrap::afterPropertiesSet); } @@ -223,10 +264,10 @@ void cannotAddUserWithNoPassword() { void noOverrideByDefault() { UaaUser joe = new UaaUser("joe", "password", "joe@test.org", "Joe", "User"); joe = joe.authorities(AuthorityUtils.commaSeparatedStringToAuthorityList("openid,read")); - ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList()); + ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); joe = new UaaUser("joe", "password", "joe@test.org", "Joel", "User"); - bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList()); + bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); @SuppressWarnings("unchecked") Collection> users = (Collection>) scimUserEndpoints.findUsers("id", @@ -243,10 +284,10 @@ void noOverrideByDefault() { void canOverride() { UaaUser joe = new UaaUser("joe", "password", "joe@test.org", "Joe", "User"); joe = joe.authorities(AuthorityUtils.commaSeparatedStringToAuthorityList("openid,read")); - ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList()); + ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); joe = new UaaUser("joe", "password", "joe@test.org", "Joel", "User"); - bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), true, Collections.emptyList()); + bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), true, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); @SuppressWarnings("unchecked") Collection> users = (Collection>) scimUserEndpoints.findUsers("id", @@ -263,10 +304,10 @@ void canOverride() { void canOverrideAuthorities() { UaaUser joe = new UaaUser("joe", "password", "joe@test.org", "Joe", "User"); joe = joe.authorities(AuthorityUtils.commaSeparatedStringToAuthorityList("openid,read")); - ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList()); + ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); joe = joe.authorities(AuthorityUtils.commaSeparatedStringToAuthorityList("openid,read,write")); - bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), true, Collections.emptyList()); + bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), true, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); @SuppressWarnings("unchecked") Collection> users = (Collection>) scimUserEndpoints.findUsers("id", @@ -285,11 +326,11 @@ void canRemoveAuthorities() { String joeUserId = "joe" + randomValueStringGenerator.generate(); UaaUser joe = new UaaUser(joeUserId, "password", "joe@test.org", "Joe", "User"); joe = joe.authorities(AuthorityUtils.commaSeparatedStringToAuthorityList("openid,read")); - ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList()); + ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); joe = joe.authorities(AuthorityUtils.commaSeparatedStringToAuthorityList("openid")); System.err.println(jdbcTemplate.queryForList("SELECT * FROM group_membership")); - bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), true, Collections.emptyList()); + bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), true, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); List users = jdbcScimUserProvisioning.query("userName eq \"" + joeUserId + "\"", IdentityZone.getUaaZoneId()); @@ -304,14 +345,14 @@ void canRemoveAuthorities() { void canUpdateUsers() { UaaUser joe = new UaaUser("joe", "password", "joe@test.org", "Joe", "User"); joe = joe.modifyOrigin(OriginKeys.UAA); - ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList()); + ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); String passwordHash = jdbcTemplate.queryForObject("select password from users where username='joe'", new Object[0], String.class); joe = new UaaUser("joe", "new", "joe@test.org", "Joe", "Bloggs"); joe = joe.modifyOrigin(OriginKeys.UAA); - bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), true, Collections.emptyList()); + bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), true, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); Collection users = jdbcScimUserProvisioning.retrieveAll(IdentityZone.getUaaZoneId()); assertEquals(1, users.size()); @@ -323,13 +364,224 @@ void canUpdateUsers() { assertEquals(passwordHash, jdbcTemplate.queryForObject("select password from users where username='joe'", new Object[0], String.class)); } + @Test + void shouldPropagateAliasPropertiesOfExistingUserDuringUpdate() { + // arrange custom zone exists + final String customZoneId = new AlphanumericRandomValueStringGenerator(8).generate(); + createCustomZone(customZoneId); + + // create a user with alias + final String originKey = new AlphanumericRandomValueStringGenerator(8).generate(); + final String userName = "john.doe-" + new AlphanumericRandomValueStringGenerator(8).generate(); + final String givenName = "John"; + final String familyName = "Doe"; + final String emailAddress = "john.doe@example.com"; + final Triple userIdsAndOriginalUser = createUserWithAlias(customZoneId, originKey, + emailAddress, userName, givenName, familyName); + final String originalUserId = userIdsAndOriginalUser.getLeft(); + final String aliasUserId = userIdsAndOriginalUser.getMiddle(); + + // create and emit event that contains the user with changed fields + final String externalId = new AlphanumericRandomValueStringGenerator(8).generate(); + final String phoneNumber = "12345"; + final UaaUserPrototype userPrototype = new UaaUserPrototype() + .withVerified(true) + .withUsername(userName) + .withPassword("") + .withEmail(emailAddress) + .withAuthorities(UaaAuthority.USER_AUTHORITIES) + .withGivenName(givenName) + .withFamilyName(familyName) + .withCreated(new Date()) + .withModified(new Date()) + .withOrigin(originKey) + .withExternalId(externalId) // changed field + .withZoneId(IdentityZone.getUaaZoneId()) + .withPhoneNumber(phoneNumber); // changed field + final UaaUser uaaUser = new UaaUser(userPrototype); + + final ExternalGroupAuthorizationEvent event = new ExternalGroupAuthorizationEvent(uaaUser, true, Collections.emptyList(), true); + final ScimUserBootstrap bootstrap = buildScimUserBootstrapWithAliasEnabled(); + bootstrap.onApplicationEvent(event); + + // should update both users and the alias reference should stay intact + final ScimUser originalUserAfterEvent = jdbcScimUserProvisioning.retrieve(originalUserId, IdentityZone.getUaaZoneId()); + assertEquals(aliasUserId, originalUserAfterEvent.getAliasId()); + assertEquals(customZoneId, originalUserAfterEvent.getAliasZid()); + assertEquals(externalId, originalUserAfterEvent.getExternalId()); + assertEquals(phoneNumber, originalUserAfterEvent.getPhoneNumbers().get(0).getValue()); + + final ScimUser aliasUserAfterEvent = jdbcScimUserProvisioning.retrieve(aliasUserId, customZoneId); + assertEquals(originalUserId, aliasUserAfterEvent.getAliasId()); + assertEquals(IdentityZone.getUaaZoneId(), aliasUserAfterEvent.getAliasZid()); + assertEquals(externalId, aliasUserAfterEvent.getExternalId()); + assertEquals(phoneNumber, aliasUserAfterEvent.getPhoneNumbers().get(0).getValue()); + } + + @Test + void shouldOnlyUpdateOriginalUser_WhenUserHasAliasButAliasEntitiesDisabled() { + // arrange custom zone exists + final String customZoneId = new AlphanumericRandomValueStringGenerator(8).generate(); + createCustomZone(customZoneId); + + // create a user with alias + final String originKey = new AlphanumericRandomValueStringGenerator(8).generate(); + final String userName = "john.doe-" + new AlphanumericRandomValueStringGenerator(8).generate(); + final String givenName = "John"; + final String familyName = "Doe"; + final String emailAddress = "john.doe@example.com"; + final Triple userIdsAndOriginalUser = createUserWithAlias(customZoneId, originKey, + emailAddress, userName, givenName, familyName); + final String originalUserId = userIdsAndOriginalUser.getLeft(); + final String aliasUserId = userIdsAndOriginalUser.getMiddle(); + final ScimUser originalUser = userIdsAndOriginalUser.getRight(); + + // create and emit event that contains the user with changed fields + final String externalId = new AlphanumericRandomValueStringGenerator(8).generate(); + final String phoneNumber = "12345"; + final UaaUserPrototype userPrototype = new UaaUserPrototype() + .withVerified(true) + .withUsername(userName) + .withPassword("") + .withEmail(emailAddress) + .withAuthorities(UaaAuthority.USER_AUTHORITIES) + .withGivenName(givenName) + .withFamilyName(familyName) + .withCreated(new Date()) + .withModified(new Date()) + .withOrigin(originKey) + .withExternalId(externalId) // changed field + .withZoneId(IdentityZone.getUaaZoneId()) + .withPhoneNumber(phoneNumber); // changed field + final UaaUser uaaUser = new UaaUser(userPrototype); + + final ExternalGroupAuthorizationEvent event = new ExternalGroupAuthorizationEvent(uaaUser, true, Collections.emptyList(), true); + final ScimUserBootstrap bootstrapAliasDisabled = new ScimUserBootstrap( + jdbcScimUserProvisioning, + scimUserService, + jdbcScimGroupProvisioning, + jdbcScimGroupMembershipManager, + Collections.emptyList(), + false, + Collections.emptyList(), + false + ); + bootstrapAliasDisabled.onApplicationEvent(event); + + // should only update the original user and the alias reference should stay intact + final ScimUser originalUserAfterEvent = jdbcScimUserProvisioning.retrieve(originalUserId, IdentityZone.getUaaZoneId()); + assertEquals(aliasUserId, originalUserAfterEvent.getAliasId()); + assertEquals(customZoneId, originalUserAfterEvent.getAliasZid()); + assertEquals(externalId, originalUserAfterEvent.getExternalId()); + assertEquals(phoneNumber, originalUserAfterEvent.getPhoneNumbers().get(0).getValue()); + + assertNotEquals(originalUser.getExternalId(), originalUserAfterEvent.getExternalId()); + assertNotEquals(originalUser.getPhoneNumbers(), originalUserAfterEvent.getPhoneNumbers()); + + final ScimUser aliasUserAfterEvent = jdbcScimUserProvisioning.retrieve(aliasUserId, customZoneId); + assertEquals(originalUserId, aliasUserAfterEvent.getAliasId()); + assertEquals(IdentityZone.getUaaZoneId(), aliasUserAfterEvent.getAliasZid()); + assertEquals(originalUser.getExternalId(), aliasUserAfterEvent.getExternalId()); // should be left unchanged + assertEquals(originalUser.getPhoneNumbers(), aliasUserAfterEvent.getPhoneNumbers()); // should be left unchanged + } + + private void createCustomZone(final String customZoneId) { + final IdentityZone customZone = new IdentityZone(); + customZone.setId(customZoneId); + customZone.setSubdomain(customZoneId); + customZone.setName(customZoneId); + identityZoneProvisioning.create(customZone); + } + + /** + * @return a triple of the original user's ID, the alias user's ID and the original user + */ + private Triple createUserWithAlias( + final String customZoneId, + final String originKey, + final String emailAddress, + final String userName, + final String givenName, + final String familyName + ) { + // arrange that a user with alias exists + final ScimUser scimUser = new ScimUser(null, userName, givenName, familyName); + final ScimUser.Email email = new ScimUser.Email(); + email.setPrimary(true); + email.setValue(emailAddress); + scimUser.setEmails(Collections.singletonList(email)); + scimUser.setOrigin(originKey); + scimUser.setZoneId(IdentityZone.getUaaZoneId()); + final ScimUser createdOriginalUser = jdbcScimUserProvisioning.createUser(scimUser, "", IdentityZone.getUaaZoneId()); + final String originalUserId = createdOriginalUser.getId(); + assertTrue(StringUtils.hasText(originalUserId)); + + // create an alias of the user in the custom zone + createdOriginalUser.setId(null); + createdOriginalUser.setZoneId(customZoneId); + createdOriginalUser.setAliasId(originalUserId); + createdOriginalUser.setAliasZid(IdentityZone.getUaaZoneId()); + final ScimUser createdAliasUser = jdbcScimUserProvisioning.createUser(createdOriginalUser, "", customZoneId); + final String aliasUserId = createdAliasUser.getId(); + assertTrue(StringUtils.hasText(aliasUserId)); + + // update the original user to point ot the alias user + createdOriginalUser.setId(originalUserId); + createdOriginalUser.setZoneId(IdentityZone.getUaaZoneId()); + createdOriginalUser.setAliasId(aliasUserId); + createdOriginalUser.setAliasZid(customZoneId); + final ScimUser originalUser = jdbcScimUserProvisioning.update(originalUserId, createdOriginalUser, IdentityZone.getUaaZoneId()); + + return Triple.of(originalUserId, aliasUserId, originalUser); + } + + private ScimUserBootstrap buildScimUserBootstrapWithAliasEnabled() { + final ScimUserService scimUserServiceAliasEnabled = buildScimUserServiceAliasEnabled(); + return new ScimUserBootstrap( + jdbcScimUserProvisioning, + scimUserServiceAliasEnabled, + jdbcScimGroupProvisioning, + jdbcScimGroupMembershipManager, + Collections.emptyList(), + false, + Collections.emptyList(), + true + ); + } + + private ScimUserService buildScimUserServiceAliasEnabled() { + final ScimUserAliasHandler aliasHandlerAliasEnabled = buildScimUserAliasHandlerAliasEnabled(); + final TransactionTemplate txTemplate = mock(TransactionTemplate.class); + when(txTemplate.execute(any())).then(invocationOnMock -> { + final TransactionCallback action = invocationOnMock.getArgument(0); + return action.doInTransaction(mock(TransactionStatus.class)); + }); + return new ScimUserService( + aliasHandlerAliasEnabled, + jdbcScimUserProvisioning, + identityZoneManager, + txTemplate, + true + ); + } + + private ScimUserAliasHandler buildScimUserAliasHandlerAliasEnabled() { + return new ScimUserAliasHandler( + identityZoneProvisioning, + jdbcScimUserProvisioning, + idpProvisioning, + identityZoneManager, + true + ); + } + @Test void unsuccessfulAttemptToUpdateUsersNotFatal() { UaaUser joe = new UaaUser("joe", "password", "joe@test.org", "Joe", "User"); - ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList()); + ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); joe = new UaaUser("joe", "new", "joe@test.org", "Joe", "Bloggs"); - bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList()); + bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); Collection users = jdbcScimUserProvisioning.retrieveAll(IdentityZone.getUaaZoneId()); assertEquals(1, users.size()); @@ -340,14 +592,14 @@ void unsuccessfulAttemptToUpdateUsersNotFatal() { void updateUserWithEmptyPasswordDoesNotChangePassword() { UaaUser joe = new UaaUser("joe", "password", "joe@test.org", "Joe", "User"); joe = joe.modifyOrigin(OriginKeys.UAA); - ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList()); + ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), false, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); String passwordHash = jdbcTemplate.queryForObject("select password from users where username='joe'", new Object[0], String.class); joe = new UaaUser("joe", "", "joe@test.org", "Joe", "Bloggs"); joe = joe.modifyOrigin(OriginKeys.UAA); - bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), true, Collections.emptyList()); + bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(joe), true, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); Collection users = jdbcScimUserProvisioning.retrieveAll(IdentityZone.getUaaZoneId()); assertEquals(1, users.size()); @@ -366,7 +618,7 @@ void uaaUserGetsVerifiedSetToTrue() { String username = new RandomValueStringGenerator().generate().toLowerCase(); UaaUser user = getUaaUser(new String[0], origin, email, firstName, lastName, password, externalId, "not-used-id", username); - ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(user), false, Collections.emptyList()); + ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(user), false, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); ScimUser existingUser = jdbcScimUserProvisioning.retrieveAll(IdentityZone.getUaaZoneId()) @@ -398,7 +650,7 @@ void externalInvitedUserGetsVerifiedSetToFalse() { String username = new RandomValueStringGenerator().generate().toLowerCase(); UaaUser user = getUaaUser(new String[0], origin, email, firstName, lastName, password, externalId, "not-used-id", username); - ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(user), false, Collections.emptyList()); + ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(user), false, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); ScimUser existingUser = jdbcScimUserProvisioning.retrieveAll(IdentityZone.getUaaZoneId()) @@ -420,12 +672,12 @@ void externalInvitedUserGetsVerifiedSetToFalse() { @Test void canAddNonExistentGroupThroughEvent() throws Exception { - nonExistentGroupThroughEvent(true, jdbcTemplate, jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager); + nonExistentGroupThroughEvent(true, jdbcTemplate, jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, scimUserService); } @Test void doNotAddNonExistentUsers() throws Exception { - nonExistentGroupThroughEvent(false, jdbcTemplate, jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager); + nonExistentGroupThroughEvent(false, jdbcTemplate, jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, scimUserService); } @Test @@ -443,7 +695,7 @@ void canUpdateEmailThroughEvent() { String userId = new RandomValueStringGenerator().generate(); String username = new RandomValueStringGenerator().generate(); UaaUser user = getUaaUser(userAuthorities, origin, email, firstName, lastName, password, externalId, userId, username); - ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(user), false, Collections.emptyList()); + ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(user), false, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); List users = jdbcScimUserProvisioning.query("userName eq \"" + username + "\" and origin eq \"" + origin + "\"", IdentityZone.getUaaZoneId()); @@ -491,7 +743,7 @@ void testGroupsFromEventAreMadeUnique() { String username = new RandomValueStringGenerator().generate(); UaaUser user = getUaaUser(userAuthorities, origin, email, firstName, lastName, password, externalId, userId, username); JdbcScimGroupMembershipManager spy = spy(jdbcScimGroupMembershipManager); - ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, spy, Collections.singletonList(user), false, Collections.emptyList()); + ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, spy, Collections.singletonList(user), false, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); List users = jdbcScimUserProvisioning.query("userName eq \"" + username + "\" and origin eq \"" + origin + "\"", IdentityZone.getUaaZoneId()); @@ -521,11 +773,11 @@ void addUsersWithSameUsername() { String userId = new RandomValueStringGenerator().generate(); String username = new RandomValueStringGenerator().generate(); UaaUser user = getUaaUser(new String[0], origin, email, firstName, lastName, password, externalId, userId, username); - ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(user), false, Collections.emptyList()); + ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(user), false, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); addIdentityProvider(jdbcTemplate, "newOrigin"); - bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Arrays.asList(user, user.modifySource("newOrigin", "")), false, Collections.emptyList()); + bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Arrays.asList(user, user.modifySource("newOrigin", "")), false, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); assertEquals(2, jdbcScimUserProvisioning.retrieveAll(IdentityZone.getUaaZoneId()).size()); } @@ -547,7 +799,7 @@ void concurrentAuthEventsRaceCondition() throws Exception { String userId = new RandomValueStringGenerator().generate(); String username = new RandomValueStringGenerator().generate(); UaaUser user = getUaaUser(userAuthorities, origin, email, firstName, lastName, password, externalId, userId, username); - ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(user), false, Collections.emptyList()); + ScimUserBootstrap bootstrap = new ScimUserBootstrap(jdbcScimUserProvisioning, scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(user), false, Collections.emptyList(), false); bootstrap.afterPropertiesSet(); List scimUsers = jdbcScimUserProvisioning.query("userName eq \"" + username + "\" and origin eq \"" + origin + "\"", IdentityZone.getUaaZoneId()); @@ -587,16 +839,21 @@ private static void canAddUsers( String zoneId, JdbcScimUserProvisioning jdbcScimUserProvisioning, JdbcScimGroupProvisioning jdbcScimGroupProvisioning, - JdbcScimGroupMembershipManager jdbcScimGroupMembershipManager) { + JdbcScimGroupMembershipManager jdbcScimGroupMembershipManager, + ScimUserService scimUserService + ) { UaaUser joe = new UaaUser("joe", "password", "joe@test.org", "Joe", "User", origin, zoneId); UaaUser mabel = new UaaUser("mabel", "password", "mabel@blah.com", "Mabel", "User", origin, zoneId); ScimUserBootstrap bootstrap = new ScimUserBootstrap( jdbcScimUserProvisioning, + scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Arrays.asList(joe, mabel), false, - Collections.emptyList()); + Collections.emptyList(), + false + ); bootstrap.afterPropertiesSet(); } @@ -605,7 +862,9 @@ private static void nonExistentGroupThroughEvent( final JdbcTemplate jdbcTemplate, final JdbcScimUserProvisioning jdbcScimUserProvisioning, final JdbcScimGroupProvisioning jdbcScimGroupProvisioning, - final JdbcScimGroupMembershipManager jdbcScimGroupMembershipManager) { + final JdbcScimGroupMembershipManager jdbcScimGroupMembershipManager, + final ScimUserService scimUserService + ) { String[] externalAuthorities = new String[]{"extTest1", "extTest2", "extTest3"}; String[] userAuthorities = new String[]{"usrTest1", "usrTest2", "usrTest3"}; String origin = "testOrigin"; @@ -620,11 +879,14 @@ private static void nonExistentGroupThroughEvent( UaaUser user = getUaaUser(userAuthorities, origin, email, firstName, lastName, password, externalId, userId, username); ScimUserBootstrap bootstrap = new ScimUserBootstrap( jdbcScimUserProvisioning, + scimUserService, jdbcScimGroupProvisioning, jdbcScimGroupMembershipManager, Collections.singletonList(user), false, - Collections.emptyList()); + Collections.emptyList(), + false + ); bootstrap.afterPropertiesSet(); List users = jdbcScimUserProvisioning.query("userName eq \"" + username + "\" and origin eq \"" + origin + "\"", IdentityZone.getUaaZoneId()); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsAliasTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsAliasTests.java index f77ab48f9f3..f78d2a5b27b 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsAliasTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsAliasTests.java @@ -14,6 +14,7 @@ import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; import org.cloudfoundry.identity.uaa.scim.exception.ScimException; import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceConflictException; +import org.cloudfoundry.identity.uaa.scim.services.ScimUserService; import org.cloudfoundry.identity.uaa.scim.validate.PasswordValidator; import org.cloudfoundry.identity.uaa.security.IsSelfCheck; import org.cloudfoundry.identity.uaa.util.AlphanumericRandomValueStringGenerator; @@ -89,6 +90,14 @@ class ScimUserEndpointsAliasTests { @BeforeEach void setUp() { + final ScimUserService scimUserService = new ScimUserService( + scimUserAliasHandler, + scimUserProvisioning, + identityZoneManager, + transactionTemplate, + true // alias entities are enabled + ); + scimUserEndpoints = new ScimUserEndpoints( identityZoneManager, isSelfCheck, @@ -100,6 +109,7 @@ void setUp() { expiringCodeStore, approvalStore, scimGroupMembershipManager, + scimUserService, scimUserAliasHandler, transactionTemplate, true, // alias entities are enabled diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/scim/services/ScimUserServiceTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/services/ScimUserServiceTest.java new file mode 100644 index 00000000000..dbfe155533f --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/scim/services/ScimUserServiceTest.java @@ -0,0 +1,222 @@ +package org.cloudfoundry.identity.uaa.scim.services; + +import org.cloudfoundry.identity.uaa.alias.AliasPropertiesInvalidException; +import org.cloudfoundry.identity.uaa.scim.ScimUser; +import org.cloudfoundry.identity.uaa.scim.ScimUserAliasHandler; +import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning; +import org.cloudfoundry.identity.uaa.util.AlphanumericRandomValueStringGenerator; +import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager; +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; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ScimUserServiceTest { + @Mock + private ScimUserAliasHandler scimUserAliasHandler; + + @Mock + private ScimUserProvisioning scimUserProvisioning; + + @Mock + private IdentityZoneManager identityZoneManager; + + @Mock + private TransactionTemplate transactionTemplate; + + private ScimUserService scimUserService; + + private static final AlphanumericRandomValueStringGenerator RANDOM_STRING_GENERATOR = + new AlphanumericRandomValueStringGenerator(8); + + private final String idzId = RANDOM_STRING_GENERATOR.generate(); + private final String userId = RANDOM_STRING_GENERATOR.generate(); + private final String origin = RANDOM_STRING_GENERATOR.generate(); + + @BeforeEach + void setUp() { + // mock current IdZ + when(identityZoneManager.getCurrentIdentityZoneId()).thenReturn(idzId); + } + + /** + * Test cases for both alias entities being enabled and disabled. + */ + private abstract class Base { + @Test + final void testUpdate_ShouldThrow_WhenAliasPropertiesAreInvalid() { + // mock existing user + final ScimUser existingUser = mock(ScimUser.class); + when(scimUserProvisioning.retrieve(userId, idzId)).thenReturn(existingUser); + + // arrange alias properties are invalid + final ScimUser user = mock(ScimUser.class); + when(scimUserAliasHandler.aliasPropertiesAreValid(user, existingUser)).thenReturn(false); + + assertThatExceptionOfType(AliasPropertiesInvalidException.class) + .isThrownBy(() -> scimUserService.updateUser(userId, user)); + } + } + + @Nested + class AliasEntitiesEnabled extends Base { + @BeforeEach + void setUp() { + scimUserService = new ScimUserService( + scimUserAliasHandler, + scimUserProvisioning, + identityZoneManager, + transactionTemplate, + true + ); + + // mock transaction template + lenient().when(transactionTemplate.execute(ArgumentMatchers.any())) + .then(invocationOnMock -> { + final TransactionCallback callback = invocationOnMock.getArgument(0); + return callback.doInTransaction(mock(TransactionStatus.class)); + }); + } + + @Test + void testUpdate_ShouldAlsoUpdateAlias_WhenAliasPropertiesAreValid() { + // mock existing user + final ScimUser existingUser = buildExemplaryUser(userId, idzId, origin); + when(scimUserProvisioning.retrieve(userId, idzId)).thenReturn(existingUser); + + // arrange alias properties are valid + final ScimUser user = cloneScimUser(existingUser); + user.setUserName("%s-updated".formatted(user.getUserName())); + when(scimUserAliasHandler.aliasPropertiesAreValid(user, existingUser)).thenReturn(true); + + // arrange update of original user + final ScimUser updatedOriginalUser = mock(ScimUser.class); + when(scimUserProvisioning.update(userId, user, idzId)).thenReturn(updatedOriginalUser); + + scimUserService.updateUser(userId, user); + + // scimUserProvisioning.update should be called exactly once + verify(scimUserProvisioning, times(1)).update(userId, user, idzId); + + // the scim alias handler should be called + verify(scimUserAliasHandler, times(1)).ensureConsistencyOfAliasEntity( + updatedOriginalUser, + existingUser + ); + } + } + + @Nested + class AliasEntitiesDisabled extends Base { + @BeforeEach + void setUp() { + scimUserService = new ScimUserService( + scimUserAliasHandler, + scimUserProvisioning, + identityZoneManager, + transactionTemplate, + false + ); + } + + @Test + void testUpdate_ShouldUpdateOnlyOriginalUser_WhenAliasEnabledAndPropertiesAreValid() { + // mock existing user + final ScimUser existingUser = buildExemplaryUser(userId, idzId, origin); + when(scimUserProvisioning.retrieve(userId, idzId)).thenReturn(existingUser); + + // arrange alias properties are valid + final ScimUser user = cloneScimUser(existingUser); + user.setUserName("%s-updated".formatted(user.getUserName())); + when(scimUserAliasHandler.aliasPropertiesAreValid(user, existingUser)).thenReturn(true); + + scimUserService.updateUser(userId, user); + + // scimUserProvisioning.update should be called exactly once + verify(scimUserProvisioning, times(1)).update(userId, user, idzId); + + // the scim alias handler should not be called + verify(scimUserAliasHandler, never()).ensureConsistencyOfAliasEntity(any(), any()); + } + } + + private static ScimUser buildExemplaryUser( + @Nullable final String id, + @Nonnull final String idzId, + @Nonnull final String origin + ) { + final ScimUser user = new ScimUser(); + user.setId(id); + user.setZoneId(idzId); + user.setName(new ScimUser.Name("John", "Doe")); + final String userName = "john.doe." + RANDOM_STRING_GENERATOR.generate(); + user.setUserName(userName); + final ScimUser.Email email = new ScimUser.Email(); + email.setPrimary(true); + email.setValue("%s@example.com".formatted(userName)); + user.setEmails(singletonList(email)); + user.setActive(true); + user.setOrigin(origin); + return user; + } + + private static ScimUser cloneScimUser(final ScimUser user) { + final ScimUser clone = new ScimUser(); + clone.setId(user.getId()); + clone.setExternalId(user.getExternalId()); + clone.setUserName(user.getUserName()); + clone.setEmails(user.getEmails().stream().map(it -> { + final ScimUser.Email email = new ScimUser.Email(); + email.setValue(it.getValue()); + email.setPrimary(it.isPrimary()); + email.setType(it.getType()); + return email; + }).toList()); + clone.setName(new ScimUser.Name(user.getName().getGivenName(), user.getName().getFamilyName())); + final List clonedPhoneNumbers; + if (user.getPhoneNumbers() == null) { + clonedPhoneNumbers = null; + } else { + clonedPhoneNumbers = user.getPhoneNumbers().stream().map(it -> { + final ScimUser.PhoneNumber phoneNumber = new ScimUser.PhoneNumber(); + phoneNumber.setType(it.getType()); + phoneNumber.setValue(it.getValue()); + return phoneNumber; + }).toList(); + } + clone.setPhoneNumbers(clonedPhoneNumbers); + clone.setActive(user.isActive()); + clone.setOrigin(user.getOrigin()); + clone.setAliasId(user.getAliasId()); + clone.setAliasZid(user.getAliasZid()); + clone.setZoneId(user.getZoneId()); + clone.setPassword(user.getPassword()); + clone.setSalt(user.getSalt()); + clone.setLastLogonTime(user.getLastLogonTime()); + clone.setPasswordLastModified(user.getPasswordLastModified()); + clone.setPreviousLogonTime(user.getPreviousLogonTime()); + return clone; + } +} \ No newline at end of file diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsAliasMockMvcTests.java index df284a48dc7..429a5d6426c 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsAliasMockMvcTests.java @@ -14,6 +14,7 @@ import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.scim.ScimUserAliasHandler; import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimUserProvisioning; +import org.cloudfoundry.identity.uaa.scim.services.ScimUserService; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneConfiguration; @@ -55,6 +56,7 @@ public class ScimUserEndpointsAliasMockMvcTests extends AliasMockMvcTestBase { private IdentityProviderEndpoints identityProviderEndpoints; private ScimUserAliasHandler scimUserAliasHandler; private ScimUserEndpoints scimUserEndpoints; + private ScimUserService scimUserService; @BeforeEach void setUp() throws Exception { @@ -64,6 +66,7 @@ void setUp() throws Exception { identityProviderEndpoints = requireNonNull(webApplicationContext.getBean(IdentityProviderEndpoints.class)); scimUserAliasHandler = requireNonNull(webApplicationContext.getBean(ScimUserAliasHandler.class)); scimUserEndpoints = requireNonNull(webApplicationContext.getBean(ScimUserEndpoints.class)); + scimUserService = requireNonNull(webApplicationContext.getBean(ScimUserService.class)); } @Nested @@ -1707,5 +1710,6 @@ protected void arrangeAliasFeatureEnabled(final boolean enabled) { ReflectionTestUtils.setField(identityProviderEndpoints, "aliasEntitiesEnabled", enabled); ReflectionTestUtils.setField(scimUserAliasHandler, "aliasEntitiesEnabled", enabled); ReflectionTestUtils.setField(scimUserEndpoints, "aliasEntitiesEnabled", enabled); + ReflectionTestUtils.setField(scimUserService, "aliasEntitiesEnabled", enabled); } } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsTests.java index aee57bb05ab..c0338ae02ae 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/scim/endpoints/ScimUserEndpointsTests.java @@ -32,6 +32,7 @@ import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceNotFoundException; import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimGroupProvisioning; import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimUserProvisioning; +import org.cloudfoundry.identity.uaa.scim.services.ScimUserService; import org.cloudfoundry.identity.uaa.scim.validate.PasswordValidator; import org.cloudfoundry.identity.uaa.security.IsSelfCheck; import org.cloudfoundry.identity.uaa.test.ZoneSeeder; @@ -216,6 +217,14 @@ void setUpAfterSeeding(final IdentityZone identityZone) { .then(invocationOnMock -> invocationOnMock.getArgument(0)); when(scimUserAliasHandler.retrieveAliasEntity(any())).thenReturn(Optional.empty()); + final ScimUserService scimUserService = new ScimUserService( + scimUserAliasHandler, + jdbcScimUserProvisioning, + identityZoneManager, + transactionTemplate, + false + ); + scimUserEndpoints = new ScimUserEndpoints( new IdentityZoneManagerImpl(), new IsSelfCheck(null), @@ -227,6 +236,7 @@ void setUpAfterSeeding(final IdentityZone identityZone) { null, mockApprovalStore, spiedScimGroupMembershipManager, + scimUserService, scimUserAliasHandler, transactionTemplate, false, @@ -721,7 +731,7 @@ void findUsersApprovalsNotSyncedIfNotIncluded() { void whenSettingAnInvalidUserMaxCount_ScimUsersEndpointShouldThrowAnException() { assertThrowsWithMessageThat( IllegalArgumentException.class, - () -> new ScimUserEndpoints(null, null, null, null, null, null, null, null, null, null, null, null, false, 0), + () -> new ScimUserEndpoints(null, null, null, null, null, null, null, null, null, null, null, null, null, false, 0), containsString("Invalid \"userMaxCount\" value (got 0). Should be positive number.")); } @@ -729,7 +739,7 @@ void whenSettingAnInvalidUserMaxCount_ScimUsersEndpointShouldThrowAnException() void whenSettingANegativeValueUserMaxCount_ScimUsersEndpointShouldThrowAnException() { assertThrowsWithMessageThat( IllegalArgumentException.class, - () -> new ScimUserEndpoints(null, null, null, null, null, null, null, null, null, null, null, null, false, -1), + () -> new ScimUserEndpoints(null, null, null, null, null, null, null, null, null, null, null, null, null, false, -1), containsString("Invalid \"userMaxCount\" value (got -1). Should be positive number.")); }