diff --git a/model/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserTests.java b/model/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserTests.java index ae0bb3d8e8c..c590f564585 100644 --- a/model/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserTests.java +++ b/model/src/test/java/org/cloudfoundry/identity/uaa/scim/ScimUserTests.java @@ -827,6 +827,14 @@ public void testPatchActive() { assertTrue(patchUser.isActive()); } + @Test + public void testScimUserAliasDeserialization() { + user.setAliasId("aliasId"); + user.setAliasZid("custom"); + String staticJson = "{\"id\":\"id\",\"externalId\":\"\",\"meta\":{\"version\":0},\"userName\":\"uname\",\"name\":{\"formatted\":\"gname fname\",\"familyName\":\"fname\",\"givenName\":\"gname\"},\"emails\":[{\"value\":\"test@example.org\",\"primary\":false}],\"phoneNumbers\":[{\"value\":\"0123456789\"}],\"displayName\":\"display\",\"title\":\"title\",\"locale\":\"en.UTF-8\",\"active\":true,\"verified\":true,\"origin\":\"\",\"aliasZid\":\"custom\",\"aliasId\":\"aliasId\",\"password\":\"password\",\"schemas\":[\"urn:scim:schemas:core:1.0\"]}"; + assertEquals(user, JsonUtils.readValue(staticJson, ScimUser.class)); + } + @Test public void testPatchVerified() { user.setVerified(false); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderProvisioning.java index 0349fe729f6..28c73ec1338 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderProvisioning.java @@ -21,6 +21,8 @@ public interface IdentityProviderProvisioning { IdentityProvider update(IdentityProvider identityProvider, String zoneId); + boolean idpWithAliasExistsInZone(String zoneId); + IdentityProvider retrieve(String id, String zoneId); List retrieveActive(String zoneId); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioning.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioning.java index 89f8eedf1de..6549b1bc203 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioning.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioning.java @@ -1,5 +1,7 @@ package org.cloudfoundry.identity.uaa.provider; +import static java.sql.Types.VARCHAR; + import org.cloudfoundry.identity.uaa.audit.event.SystemDeletable; import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.util.JsonUtils; @@ -11,7 +13,6 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Component; -import org.springframework.util.Assert; import org.springframework.util.StringUtils; import java.sql.ResultSet; @@ -34,13 +35,15 @@ public class JdbcIdentityProviderProvisioning implements IdentityProviderProvisi public static final String IDENTITY_ACTIVE_PROVIDERS_QUERY = IDENTITY_PROVIDERS_QUERY + " and active=?"; + public static final String IDP_WITH_ALIAS_EXISTS_QUERY = "select 1 from identity_provider idp where idp.identity_zone_id = ? and idp.alias_zid is not null and idp.alias_zid <> '' limit 1"; + public static final String ID_PROVIDER_UPDATE_FIELDS = "version,lastmodified,name,type,config,active,alias_id,alias_zid".replace(",", "=?,") + "=?"; public static final String UPDATE_IDENTITY_PROVIDER_SQL = "update identity_provider set " + ID_PROVIDER_UPDATE_FIELDS + " where id=? and identity_zone_id=?"; public static final String DELETE_IDENTITY_PROVIDER_BY_ORIGIN_SQL = "delete from identity_provider where identity_zone_id=? and origin_key = ?"; - public static final String DELETE_IDENTITY_PROVIDER_BY_ZONE_SQL = "delete from identity_provider where identity_zone_id=? or alias_zid=?"; + public static final String DELETE_IDENTITY_PROVIDER_BY_ZONE_SQL = "delete from identity_provider where identity_zone_id=?"; public static final String IDENTITY_PROVIDER_BY_ID_QUERY = "select " + ID_PROVIDER_FIELDS + " from identity_provider " + "where id=? and identity_zone_id=?"; @@ -56,6 +59,18 @@ public JdbcIdentityProviderProvisioning(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } + @Override + public boolean idpWithAliasExistsInZone(final String zoneId) { + final List result = jdbcTemplate.queryForList( + IDP_WITH_ALIAS_EXISTS_QUERY, + new Object[]{zoneId}, + new int[]{VARCHAR}, + Integer.class + ); + // if an IdP with alias is present, the list contains a single element, otherwise it is empty + return result.size() == 1; + } + @Override public IdentityProvider retrieve(String id, String zoneId) { return jdbcTemplate.queryForObject(IDENTITY_PROVIDER_BY_ID_QUERY, mapper, id, zoneId); @@ -150,12 +165,9 @@ protected void validate(IdentityProvider provider) { } } - /** - * Delete all identity providers in the given zone as well as all alias identity providers of them. - */ @Override public int deleteByIdentityZone(String zoneId) { - return jdbcTemplate.update(DELETE_IDENTITY_PROVIDER_BY_ZONE_SQL, zoneId, zoneId); + return jdbcTemplate.update(DELETE_IDENTITY_PROVIDER_BY_ZONE_SQL, zoneId); } @Override diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfigurator.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfigurator.java index 7ae1f6ec846..2dbcec883e8 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfigurator.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfigurator.java @@ -135,6 +135,11 @@ public IdentityProvider update(IdentityProvider identityProvider, String zoneId) return providerProvisioning.update(identityProvider, zoneId); } + @Override + public boolean idpWithAliasExistsInZone(final String zoneId) { + return providerProvisioning.idpWithAliasExistsInZone(zoneId); + } + @Override public IdentityProvider retrieve(String id, String zoneId) { IdentityProvider p = providerProvisioning.retrieve(id, zoneId); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneEndpoints.java b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneEndpoints.java index 346b485f9af..21ebc8ada1f 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneEndpoints.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneEndpoints.java @@ -336,7 +336,15 @@ public ResponseEntity deleteIdentityZone(@PathVariable String id) IdentityZone zone = zoneDao.retrieveIgnoreActiveFlag(id); // ignore the id in the body, the id in the path is the only one that matters IdentityZoneHolder.set(zone); - if (publisher != null && zone != null) { + + /* reject deletion if an IdP with alias exists in the zone - checking for users with alias is not required + * here, since they can only exist if their origin IdP has an alias as well */ + final boolean idpWithAliasExists = idpDao.idpWithAliasExistsInZone(zone.getId()); + if (idpWithAliasExists) { + return new ResponseEntity<>(UNPROCESSABLE_ENTITY); + } + + if (publisher != null) { publisher.publishEvent(new EntityDeletedEvent<>(zone, SecurityContextHolder.getContext().getAuthentication(), IdentityZoneHolder.getCurrentZoneId())); logger.debug("Zone - deleted id[" + zone.getId() + "]"); return new ResponseEntity<>(removeKeys(zone), OK); diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V4_109__AliasZid_Indexes.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V4_109__AliasZid_Indexes.sql new file mode 100644 index 00000000000..658d3531e20 --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V4_109__AliasZid_Indexes.sql @@ -0,0 +1,2 @@ +CREATE INDEX identity_provider_alias_zid__idx ON identity_provider (alias_zid); +CREATE INDEX users_alias_zid__idx ON users (alias_zid); diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_109__AliasZid_Indexes.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_109__AliasZid_Indexes.sql new file mode 100644 index 00000000000..658d3531e20 --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_109__AliasZid_Indexes.sql @@ -0,0 +1,2 @@ +CREATE INDEX identity_provider_alias_zid__idx ON identity_provider (alias_zid); +CREATE INDEX users_alias_zid__idx ON users (alias_zid); diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V4_109__AliasZid_Indexes.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V4_109__AliasZid_Indexes.sql new file mode 100644 index 00000000000..7c8ee9a8b4b --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V4_109__AliasZid_Indexes.sql @@ -0,0 +1,2 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS identity_provider_alias_zid__idx on identity_provider (alias_zid); +CREATE INDEX CONCURRENTLY IF NOT EXISTS users_alias_zid__idx on users (alias_zid); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerValidationTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerValidationTest.java index f4a44d3063e..6552d38f144 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerValidationTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/alias/EntityAliasHandlerValidationTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.cloudfoundry.identity.uaa.alias.EntityAliasHandlerValidationTest.NoExistingAliasBase.ExistingEntityArgument.ENTITY_WITH_EMPTY_ALIAS_PROPS; +import static org.junit.Assert.assertFalse; import java.util.UUID; import java.util.stream.Stream; @@ -129,6 +130,12 @@ final void shouldReturnFalse_UpdatesOfEntitiesWithExistingAliasForbidden() { requestBody = buildEntityWithAliasProps(null, null); assertThat(aliasHandler.aliasPropertiesAreValid(requestBody, existingEntity)).isFalse(); } + + @Test + final void shouldReturnFalse_DefaultSetting() { + AliasEntitiesConfig aliasEntitiesConfig = new AliasEntitiesConfig(); + assertFalse(aliasEntitiesConfig.aliasEntitiesEnabled(false)); + } } protected abstract class ExistingAlias_AliasFeatureEnabled extends Base { diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java index 777d51a9e56..d57818c6850 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java @@ -5,6 +5,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -69,7 +70,7 @@ void deleteProvidersInZone() { } @Test - void deleteByIdentityZone_ShouldAlsoDeleteAliasIdentityProviders() { + void deleteByIdentityZone_ShouldNotDeleteAliasIdentityProviders() { final String originSuffix = generator.generate(); // IdP 1: created in custom zone, no alias @@ -105,13 +106,13 @@ void deleteByIdentityZone_ShouldAlsoDeleteAliasIdentityProviders() { // delete by zone final int rowsDeleted = jdbcIdentityProviderProvisioning.deleteByIdentityZone(otherZoneId1); - // number should also include the alias IdP - Assertions.assertThat(rowsDeleted).isEqualTo(3); + // number should not include the alias IdP + Assertions.assertThat(rowsDeleted).isEqualTo(2); - // check if all three entries are gone + // the two IdPs in the custom zone should be deleted, the alias should still be present assertIdentityProviderDoesNotExist(createdIdp1.getId(), otherZoneId1); assertIdentityProviderDoesNotExist(createdIdp2.getId(), otherZoneId1); - assertIdentityProviderDoesNotExist(createdIdp2Alias.getId(), uaaZoneId); + assertIdentityProviderExists(createdIdp2Alias.getId(), uaaZoneId); } private void assertIdentityProviderExists(final String id, final String zoneId) { @@ -317,4 +318,26 @@ void retrieveIdentityProviderByOriginInDifferentZone() { IdentityProvider idp1 = jdbcIdentityProviderProvisioning.create(idp, otherZoneId1); assertThrows(EmptyResultDataAccessException.class, () -> jdbcIdentityProviderProvisioning.retrieveByOrigin(idp1.getOriginKey(), otherZoneId2)); } + + @Test + void testIdpWithAliasExistsInZone_TrueCase() { + final IdentityProvider idpWithAlias = MultitenancyFixture.identityProvider( + generator.generate(), + otherZoneId1 + ); + idpWithAlias.setAliasZid(IdentityZone.getUaaZoneId()); + idpWithAlias.setAliasId(UUID.randomUUID().toString()); + jdbcIdentityProviderProvisioning.create(idpWithAlias, otherZoneId1); + assertTrue(jdbcIdentityProviderProvisioning.idpWithAliasExistsInZone(otherZoneId1)); + } + + @Test + void testIdpWithAliasExistsInZone_FalseCase() { + final IdentityProvider idp = MultitenancyFixture.identityProvider( + generator.generate(), + otherZoneId2 + ); + jdbcIdentityProviderProvisioning.create(idp, otherZoneId2); + assertFalse(jdbcIdentityProviderProvisioning.idpWithAliasExistsInZone(otherZoneId2)); + } } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfiguratorTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfiguratorTests.java index e9624285b05..e7d4a8327d0 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfiguratorTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthProviderConfiguratorTests.java @@ -6,12 +6,15 @@ import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.provider.OIDCIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.RawExternalOAuthIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.util.AlphanumericRandomValueStringGenerator; import org.cloudfoundry.identity.uaa.util.UaaRandomStringUtil; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.dao.IncorrectResultSizeDataAccessException; @@ -56,6 +59,8 @@ @ExtendWith(PollutionPreventionExtension.class) @ExtendWith(MockitoExtension.class) class ExternalOAuthProviderConfiguratorTests { + private static final AlphanumericRandomValueStringGenerator RANDOM_STRING_GENERATOR = + new AlphanumericRandomValueStringGenerator(6); private final String UAA_BASE_URL = "https://localhost:8443/uaa"; @@ -386,4 +391,12 @@ void testGetIdpAuthenticationUrlAndCheckTokenFormatParameter() { UriComponentsBuilder.fromUriString(authzUri).build().getQueryParams().toSingleValueMap(); assertThat(queryParams, hasEntry("token_format", "jwt")); } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testIdpWithAliasExistsInZone(final boolean resultFromDelegate) { + final String zoneId = RANDOM_STRING_GENERATOR.generate(); + when(mockIdentityProviderProvisioning.idpWithAliasExistsInZone(zoneId)).thenReturn(resultFromDelegate); + assertEquals(resultFromDelegate, configurator.idpWithAliasExistsInZone(zoneId)); + } } 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 4321c879019..f77ab48f9f3 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 @@ -13,6 +13,7 @@ import org.cloudfoundry.identity.uaa.scim.ScimUserAliasHandler; 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.validate.PasswordValidator; import org.cloudfoundry.identity.uaa.security.IsSelfCheck; import org.cloudfoundry.identity.uaa.util.AlphanumericRandomValueStringGenerator; @@ -26,6 +27,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -123,7 +125,7 @@ void setUp() { lenient().when(transactionTemplate.execute(any())).then(invocationOnMock -> { final TransactionCallback callback = invocationOnMock.getArgument(0); - return callback.doInTransaction(mock(TransactionStatus.class)); + return callback != null ? callback.doInTransaction(mock(TransactionStatus.class)) : null; }); } @@ -177,6 +179,21 @@ void shouldThrow_WhenAliasPropertiesAreInvalid() { assertThat(exception.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST); } + @Test + void shouldThrow_WhenAliasIsNotPresent() { + final ScimUser user = buildScimUser(UAA, origin); + + when(scimUserAliasHandler.aliasPropertiesAreValid(user, null)).thenReturn(true); + when(scimUserAliasHandler.ensureConsistencyOfAliasEntity(user, null)).thenReturn(null); + + final MockHttpServletRequest req = new MockHttpServletRequest(); + final MockHttpServletResponse res = new MockHttpServletResponse(); + final IllegalStateException exception = assertThrows(IllegalStateException.class, () -> + scimUserEndpoints.createUser(user, req, res) + ); + assertThat(exception.getMessage()).isEqualTo("The persisted user is not present after handling the alias."); + } + @Test void shouldReturnOriginalUser() { final ScimUser user = buildScimUser(UAA, origin); @@ -246,6 +263,28 @@ void shouldThrow_IfAliasPropertiesAreInvalid() { assertThat(exception.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST); } + @Test + void shouldThrow_IfAliasIsLocked() { + when(scimUserAliasHandler.aliasPropertiesAreValid(originalUser, existingOriginalUser)) + .thenReturn(true); + when(transactionTemplate.execute(any())).then(invocationOnMock -> { + throw new OptimisticLockingFailureException("The alias is locked."); + }); + + final ScimResourceConflictException exception = assertThrows(ScimResourceConflictException.class, () -> + scimUserEndpoints.updateUser( + originalUser, + originalUser.getId(), + "*", + new MockHttpServletRequest(), + new MockHttpServletResponse(), + null + ) + ); + assertThat(exception.getMessage()).isEqualTo("The alias is locked."); + assertThat(exception.getStatus()).isEqualTo(HttpStatus.CONFLICT); + } + @Test void shouldAlsoUpdateAliasUserIfPresent() { when(scimUserAliasHandler.aliasPropertiesAreValid(originalUser, existingOriginalUser)) @@ -467,6 +506,20 @@ void shouldThrowException_IfUserHasExistingAlias() { .isEqualTo("Could not delete user with alias since alias entities are disabled."); assertThat(exception.getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } + + @Test + void shouldDeleteUserIfPresent() { + ScimUser originalUser = buildScimUser("123456789", "uaa"); + when(scimUserProvisioning.retrieve(any(), any())).thenReturn(originalUser); + final ScimUser response = scimUserEndpoints.deleteUser( + "12345678", + null, + new MockHttpServletRequest(), + new MockHttpServletResponse() + ); + + assertScimUsersAreEqual(response, originalUser); + } } } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneEndpointsTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneEndpointsTests.java index 76f350c3e74..21571d00986 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneEndpointsTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/zone/IdentityZoneEndpointsTests.java @@ -1,11 +1,14 @@ package org.cloudfoundry.identity.uaa.zone; +import org.cloudfoundry.identity.uaa.audit.event.EntityDeletedEvent; import org.cloudfoundry.identity.uaa.error.UaaException; import org.cloudfoundry.identity.uaa.extensions.PollutionPreventionExtension; -import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; +import org.cloudfoundry.identity.uaa.provider.JdbcIdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.saml.SamlKey; import org.cloudfoundry.identity.uaa.scim.ScimGroup; import org.cloudfoundry.identity.uaa.scim.ScimGroupProvisioning; +import org.cloudfoundry.identity.uaa.util.AlphanumericRandomValueStringGenerator; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -13,6 +16,9 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.validation.BindingResult; import java.util.List; @@ -49,14 +55,22 @@ class IdentityZoneEndpointsTests { private IdentityZoneValidator mockIdentityZoneValidator; @Mock - private IdentityProviderProvisioning mockIdentityProviderProvisioning; + private JdbcIdentityProviderProvisioning mockIdentityProviderProvisioning; @Mock private IdentityZoneEndpointClientRegistrationService mockIdentityZoneEndpointClientRegistrationService; + @Mock + private ApplicationEventPublisher mockApplicationEventPublisher; + @InjectMocks private IdentityZoneEndpoints endpoints; + @BeforeEach + void setUp() { + endpoints.setApplicationEventPublisher(mockApplicationEventPublisher); + } + @Test void create_zone() throws InvalidIdentityZoneDetailsException { when(mockIdentityZoneProvisioning.create(any())).then(invocation -> invocation.getArgument(0)); @@ -169,6 +183,45 @@ void reduce_zone_allowed_groups_on_update_should_fail() throws InvalidIdentityZo is("The identity zone user configuration contains not-allowed groups.")); } + @Test + void deleteIdentityZone_ShouldReject_IfIdpWithAliasExists() { + final IdentityZone idz = new IdentityZone(); + final String idzId = new AlphanumericRandomValueStringGenerator(5).generate(); + idz.setName(idzId); + idz.setId(idzId); + idz.setSubdomain(idzId); + when(mockIdentityZoneProvisioning.retrieveIgnoreActiveFlag(idzId)).thenReturn(idz); + + // arrange IdP with alias exists in zone + when(mockIdentityProviderProvisioning.idpWithAliasExistsInZone(idzId)).thenReturn(true); + + final ResponseEntity response = endpoints.deleteIdentityZone(idzId); + assertNotNull(response); + assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, response.getStatusCode()); + } + + @Test + void deleteIdentityZone_ShouldEmitEntityDeletedEvent_WhenNoAliasIdpExists() { + final IdentityZone idz = new IdentityZone(); + final String idzId = new AlphanumericRandomValueStringGenerator(5).generate(); + idz.setName(idzId); + idz.setId(idzId); + idz.setSubdomain(idzId); + when(mockIdentityZoneProvisioning.retrieveIgnoreActiveFlag(idzId)).thenReturn(idz); + + // arrange no IdP with alias exists in zone + when(mockIdentityProviderProvisioning.idpWithAliasExistsInZone(idzId)).thenReturn(false); + + final ResponseEntity response = endpoints.deleteIdentityZone(idzId); + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatusCode()); + + final ArgumentCaptor> eventArgument = ArgumentCaptor.forClass(EntityDeletedEvent.class); + verify(mockApplicationEventPublisher).publishEvent(eventArgument.capture()); + final var capturedEvent = eventArgument.getValue(); + assertEquals(idz, capturedEvent.getDeleted()); + } + private static IdentityZone createZone() { IdentityZone zone = MultitenancyFixture.identityZone("id", "subdomain"); IdentityZoneConfiguration config = zone.getConfig(); diff --git a/uaa/slateCustomizations/source/index.html.md.erb b/uaa/slateCustomizations/source/index.html.md.erb index 86a4d05e582..d3df21708cb 100644 --- a/uaa/slateCustomizations/source/index.html.md.erb +++ b/uaa/slateCustomizations/source/index.html.md.erb @@ -871,6 +871,7 @@ _Error Codes_ | 401 | Unauthorized - Invalid token | | 403 | Forbidden - Insufficient scope (zone admins can only delete their own zone) | | 404 | Not Found - Zone does not exist | +| 422 | Unprocessable Entity - at least one IdP with alias exists in the zone | # Identity Providers diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java index 66b6e0a7656..26fb8428adc 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/zones/IdentityZoneEndpointsMockMvcTests.java @@ -11,6 +11,7 @@ import org.cloudfoundry.identity.uaa.client.UaaClientDetails; import org.cloudfoundry.identity.uaa.client.event.ClientCreateEvent; import org.cloudfoundry.identity.uaa.client.event.ClientDeleteEvent; +import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.IdentityZoneCreationResult; import org.cloudfoundry.identity.uaa.oauth.client.ClientConstants; @@ -19,6 +20,7 @@ import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.provider.JdbcIdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.resources.SearchResults; +import org.cloudfoundry.identity.uaa.provider.OIDCIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.scim.*; import org.cloudfoundry.identity.uaa.scim.event.GroupModifiedEvent; import org.cloudfoundry.identity.uaa.scim.event.UserModifiedEvent; @@ -1758,6 +1760,33 @@ void testDeleteZonePublishesEvent() throws Exception { assertThat(auditedIdentityZone, containsString(id)); } + @Test + void testDeleteZone_ShouldFail_WhenIdpWithAliasExistsInZone() throws Exception { + // create zone + final String idzId = generator.generate(); + createZone(idzId, HttpStatus.CREATED, identityClientToken, new IdentityZoneConfiguration()); + + // create IdP with alias (via DB, since alias feature is disabled by default) + final JdbcIdentityProviderProvisioning idpProvisioning = webApplicationContext + .getBean(JdbcIdentityProviderProvisioning.class); + final IdentityProvider idp = new IdentityProvider<>(); + idp.setName("some-idp"); + idp.setId(UUID.randomUUID().toString()); + idp.setIdentityZoneId(idzId); + idp.setOriginKey("some-origin-key"); + idp.setAliasZid(IdentityZone.getUaaZoneId()); + idp.setAliasId(UUID.randomUUID().toString()); + idp.setType(OriginKeys.OIDC10); + idpProvisioning.create(idp, idzId); + + // deleting zone should fail + mockMvc.perform( + delete("/identity-zones/" + idzId) + .header("Authorization", "Bearer " + identityClientToken) + .accept(APPLICATION_JSON) + ).andExpect(status().isUnprocessableEntity()); + } + @Test void testCreateAndDeleteLimitedClientInNewZoneUsingZoneEndpoint() throws Exception { String id = generator.generate();