RED-8477: SSO settings endpoint for SAML

* added more tests
* refactoring
* added check for existing idp with same displayName on update

(cherry picked from commit 12e8c0f53f57fea724402dc8be039f785a821f2d)
This commit is contained in:
maverickstuder 2024-02-21 12:01:53 +01:00 committed by Ali Oezyetimoglu
parent da59130a7e
commit 7e60a66a68
3 changed files with 280 additions and 43 deletions

View File

@ -4,6 +4,7 @@ import static com.knecon.fforesight.tenantusermanagement.permissions.UserManagem
import static com.knecon.fforesight.tenantusermanagement.permissions.UserManagementPermissions.WRITE_IDENTITY_PROVIDER_CONFIGURATION;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -38,6 +39,7 @@ import com.knecon.fforesight.tenantusermanagement.service.RealmService;
import com.knecon.fforesight.tenantusermanagement.utils.IdentityProviderMappingService;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.core.Response;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -89,10 +91,10 @@ public class IdentityProviderConfigurationController implements IdentityProvider
throw new BadRequestException("Alias must not be null.");
}
if(identityProviderRequest.getDisplayName().isEmpty()) {
if (identityProviderRequest.getDisplayName() == null || identityProviderRequest.getDisplayName().isEmpty()) {
identityProviderRequest.setDisplayName(identityProviderRequest.getAlias());
}
throwConflictExceptionIfDisplayNameAlreadyExists(identityProviderRequest.getDisplayName());
checkIfOtherIdpHasSameDisplayNameOnCreate(identityProviderRequest.getDisplayName());
IdentityProviderModel identityProviderModel = IdentityProviderMappingService.toModelFromRequest(identityProviderRequest);
return callKeyCloakIdentityProvidersCreateForModel(identityProviderModel);
@ -103,24 +105,30 @@ public class IdentityProviderConfigurationController implements IdentityProvider
@PreAuthorize("hasAuthority('" + WRITE_IDENTITY_PROVIDER_CONFIGURATION + "')")
public ResponseEntity<IdentityProviderModel> createIdentityProviderFromDescriptor(IdentityProviderWithDescriptorRequest identityProviderWithDescriptorRequest) {
if(identityProviderWithDescriptorRequest.getDisplayName().isEmpty()) {
if (identityProviderWithDescriptorRequest.getDisplayName() == null || identityProviderWithDescriptorRequest.getDisplayName().isEmpty()) {
identityProviderWithDescriptorRequest.setDisplayName(identityProviderWithDescriptorRequest.getAlias());
}
throwConflictExceptionIfDisplayNameAlreadyExists(identityProviderWithDescriptorRequest.getDisplayName());
checkIfOtherIdpHasSameDisplayNameOnCreate(identityProviderWithDescriptorRequest.getDisplayName());
Map<String, Object> requestMap = new HashMap<>();
requestMap.put("providerId", identityProviderWithDescriptorRequest.getProviderId());
requestMap.put("fromUrl", identityProviderWithDescriptorRequest.getSamlEntityDescriptorURL());
Map<String, String> configurationsMap = realmService.realm(getTenantId()).identityProviders().importFrom(requestMap);
if (configurationsMap == null || configurationsMap.isEmpty()) {
throw new BadRequestException("Could not set config from provided descriptor.");
try {
Map<String, String> configurationsMap = realmService.realm(getTenantId()).identityProviders().importFrom(requestMap);
if (configurationsMap == null || configurationsMap.isEmpty()) {
throw new BadRequestException("Could not set config from provided descriptor.");
}
configurationsMap.put("entityId", identityProviderWithDescriptorRequest.getEntityId());
IdentityProviderModel identityProviderModel = IdentityProviderMappingService.toModelFromDescriptorRequestAndConfig(identityProviderWithDescriptorRequest,
configurationsMap);
return callKeyCloakIdentityProvidersCreateForModel(identityProviderModel);
} catch (InternalServerErrorException internalServerErrorException) {
throw new BadRequestException("Provided descriptor is malformed.");
}
configurationsMap.put("entityId", identityProviderWithDescriptorRequest.getEntityId());
IdentityProviderModel identityProviderModel = IdentityProviderMappingService.toModelFromDescriptorRequestAndConfig(identityProviderWithDescriptorRequest,
configurationsMap);
return callKeyCloakIdentityProvidersCreateForModel(identityProviderModel);
}
@ -148,6 +156,9 @@ public class IdentityProviderConfigurationController implements IdentityProvider
identityProviderRequest.setAlias(identityProviderAlias);
getIdentityProviderRepresentation(identityProviderAlias);
checkIfOtherIdpHasSameDisplayNameOnUpdate(identityProviderAlias, identityProviderRequest.getDisplayName());
IdentityProviderModel identityProviderModel = IdentityProviderMappingService.toModelFromRequest(identityProviderRequest);
identityProviderModel.setSpecificDefaults();
@ -194,11 +205,26 @@ public class IdentityProviderConfigurationController implements IdentityProvider
}
private void throwConflictExceptionIfDisplayNameAlreadyExists(String displayName) {
private void checkIfOtherIdpHasSameDisplayNameOnUpdate(String alias, String displayName) {
if (getIdentityProviders().getIdentityProviders()
.stream()
.anyMatch(idp -> idp.getDisplayName().isEmpty() ? idp.getAlias().equals(displayName): idp.getDisplayName().equals(displayName))) {
checkIfOtherIdpHasSameDisplayName(getIdentityProviders().getIdentityProviders()
.stream()
.filter(identityProviderModel -> !identityProviderModel.getAlias().equals(alias))
.toList(), displayName);
}
private void checkIfOtherIdpHasSameDisplayNameOnCreate(String displayName) {
checkIfOtherIdpHasSameDisplayName(getIdentityProviders().getIdentityProviders(), displayName);
}
private void checkIfOtherIdpHasSameDisplayName(List<IdentityProviderModel> identityProviderModelList, String displayName) {
if (identityProviderModelList.stream()
.anyMatch(idp -> idp.getDisplayName().isEmpty() ? idp.getAlias().equals(displayName) : idp.getDisplayName().equals(displayName))) {
throw new ConflictException("Identity provider with this display name already exists.");
}
}

View File

@ -1,6 +1,7 @@
package com.knecon.fforesight.tenantusermanagement.model;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
@ -20,7 +21,7 @@ public class IdentityProviderRequest {
@Builder.Default
@Schema(description = "Configuration of the identity provider", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull
private IdentityProviderConfigRequest config = new IdentityProviderConfigRequest();
private @Valid IdentityProviderConfigRequest config = new IdentityProviderConfigRequest();
@Builder.Default
@Schema(description = "Display name of the identity provider in keycloak (optional, fallbacks to alias if empty)", requiredMode = Schema.RequiredMode.NOT_REQUIRED)

View File

@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@ -35,12 +36,32 @@ public class IdentityProviderConfigurationTest extends AbstractTenantUserManagem
final String idpEntityId = "https://sts.windows.net/b44be368-e4f2-4ade-a089-cd2825458048/";
final String ssoServiceUrl = "https://login.microsoftonline.com/b44be368-e4f2-4ade-a089-cd2825458048/saml2";
final String testAliasDescriptor = "testAliasDescriptor";
final String invalidUrl = "this is no valid URL :(";
final String idp1TestAlias = "idp1";
final String idp2TestAlias = "idp2";
@BeforeEach
public void beforeEachTest() {
TenantContext.setTenantId(AbstractTenantUserManagementIntegrationTest.TEST_TENANT_ID);
setupTestDataIfNotExistent();
}
private void setupTestDataIfNotExistent() {
var identityProviders = identityProviderConfigurationClient.getIdentityProviders().getIdentityProviders();
if(identityProviders.stream().noneMatch(identityProviderModel -> identityProviderModel.getAlias().equals(idp1TestAlias))) {
identityProviderConfigurationClient.createIdentityProvider(provideIdentityProviderRequestCreateForIdp1());
identityProviderConfigurationClient.createIdentityProvider(provideIdentityProviderRequestCreateForIdp2());
}
}
@Test
public void testIdentityProviderConfiguration() {
TenantContext.setTenantId(AbstractTenantUserManagementIntegrationTest.TEST_TENANT_ID);
public void testCreateEntityProviderSuccessful() {
var createdResponse = identityProviderConfigurationClient.createIdentityProvider(provideIdentityProviderRequestCreate());
assertEquals(201, createdResponse.getStatusCode().value());
@ -54,9 +75,21 @@ public class IdentityProviderConfigurationTest extends AbstractTenantUserManagem
assertTrue(identityProviderModel.getStoreToken());
assertEquals(identityProviderModel.getConfig().getSyncMode(), IdentityProviderSyncMode.FORCE);
var updatedResponse = identityProviderConfigurationClient.updateIdentityProvider(testAlias, provideIdentityProviderRequestUpdate());
var identityProviderList = identityProviderConfigurationClient.getIdentityProviders();
assertEquals(3, identityProviderList.getIdentityProviders().size());
identityProviderConfigurationClient.deleteIdentityProvider(testAlias);
identityProviderList = identityProviderConfigurationClient.getIdentityProviders();
assertEquals(2, identityProviderList.getIdentityProviders().size());
}
@Test
public void testUpdateEntityProviderSuccessful() {
var updatedResponse = identityProviderConfigurationClient.updateIdentityProvider(idp1TestAlias, provideIdentityProviderRequestUpdate());
assertEquals(200, updatedResponse.getStatusCode().value());
identityProviderModel = updatedResponse.getBody();
var identityProviderModel = updatedResponse.getBody();
assert identityProviderModel != null;
assertEquals(identityProviderModel.getConfig().getNameIDPolicyFormat(), IdentityProviderNameIDPolicyFormat.TRANSIENT);
assertEquals(identityProviderModel.getConfig().getPrincipalType(), IdentityProviderPrincipalType.ATTRIBUTE);
@ -65,10 +98,10 @@ public class IdentityProviderConfigurationTest extends AbstractTenantUserManagem
assertEquals(identityProviderModel.getConfig().getXmlSigKeyInfoKeyNameTransformer(), IdentityProviderSAMLSignatureKeyName.CERT_SUBJECT);
assertEquals(identityProviderModel.getConfig().getSignatureAlgorithm(), IdentityProviderSignatureAlgorithm.RSA_SHA512);
var identityProviderList = identityProviderConfigurationClient.getIdentityProviders();
assertEquals(identityProviderList.getIdentityProviders().size(), 1);
assertEquals(2, identityProviderList.getIdentityProviders().size());
var identityProvider = identityProviderConfigurationClient.getIdentityProvider(testAlias);
assertEquals(identityProviderModel.getAlias(), testAlias);
identityProviderModel = identityProviderConfigurationClient.getIdentityProvider(idp1TestAlias);
assertEquals(identityProviderModel.getAlias(), idp1TestAlias);
assertEquals(identityProviderModel.getConfig().getEntityId(), entityId);
assertEquals(identityProviderModel.getConfig().getIdpEntityId(), idpEntityId);
assertEquals(identityProviderModel.getConfig().getSingleSignOnServiceUrl(), ssoServiceUrl);
@ -81,38 +114,186 @@ public class IdentityProviderConfigurationTest extends AbstractTenantUserManagem
assertTrue(identityProviderModel.getConfig().getWantAuthnRequestsSigned());
assertEquals(identityProviderModel.getConfig().getXmlSigKeyInfoKeyNameTransformer(), IdentityProviderSAMLSignatureKeyName.CERT_SUBJECT);
assertEquals(identityProviderModel.getConfig().getSignatureAlgorithm(), IdentityProviderSignatureAlgorithm.RSA_SHA512);
}
@Test
public void testCreateEntityProviderFromDescriptorSuccessful() {
var createdDescriptorResponse = identityProviderConfigurationClient.createIdentityProviderFromDescriptor(provideIdentityProviderWithDescriptorRequest());
assertEquals(createdDescriptorResponse.getStatusCode().value(), 201);
assertEquals(201, createdDescriptorResponse.getStatusCode().value());
identityProviderList = identityProviderConfigurationClient.getIdentityProviders();
assertEquals(identityProviderList.getIdentityProviders().size(), 2);
var identityProviderList = identityProviderConfigurationClient.getIdentityProviders();
assertEquals(3, identityProviderList.getIdentityProviders().size());
identityProviderConfigurationClient.deleteIdentityProvider(testAliasDescriptor);
identityProviderList = identityProviderConfigurationClient.getIdentityProviders();
assertEquals(identityProviderList.getIdentityProviders().size(), 1);
assertEquals(2, identityProviderList.getIdentityProviders().size());
}
var e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.createIdentityProvider(provideIdentityProviderRequestCreate()));
@Test
public void testCreateEntityProviderDuplicated() {
String otherAlias = "otherAlias";
var e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.createIdentityProvider(provideIdentityProviderRequestCreateForIdp1()));
assertEquals(409, e.status());
e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.getIdentityProvider(testAliasDescriptor));
assertEquals(404, e.status());
var requestWithoutAlias = provideIdentityProviderRequestCreate();
requestWithoutAlias.setAlias(null);
e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.createIdentityProvider(requestWithoutAlias));
assertEquals(400, e.status());
var requestWithSameDisplayName = provideIdentityProviderRequestCreate();
requestWithSameDisplayName.setAlias("otherAlias");
requestWithSameDisplayName.setDisplayName(testAlias);
requestWithSameDisplayName.setAlias(otherAlias);
requestWithSameDisplayName.setDisplayName(idp1TestAlias);
e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.createIdentityProvider(requestWithSameDisplayName));
assertEquals(409, e.status());
var descriptorRequestWithSameDisplayName = provideIdentityProviderWithDescriptorRequest();
descriptorRequestWithSameDisplayName.setAlias(otherAlias);
descriptorRequestWithSameDisplayName.setDisplayName(idp1TestAlias);
e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.createIdentityProviderFromDescriptor(descriptorRequestWithSameDisplayName));
assertEquals(409, e.status());
var updateRequestWithDisplayNameFromOtherIdp = provideIdentityProviderRequestUpdate();
updateRequestWithDisplayNameFromOtherIdp.setDisplayName(idp1TestAlias);
e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.updateIdentityProvider(idp2TestAlias, updateRequestWithDisplayNameFromOtherIdp));
assertEquals(409, e.status());
}
@Test
public void testUpdateDeleteEntityProviderThatNotExists() {
var e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.updateIdentityProvider(testAlias, provideIdentityProviderRequestUpdate()));
assertEquals(404, e.status());
e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.deleteIdentityProvider(testAlias));
assertEquals(404, e.status());
}
@Test
public void testCreateEntityProviderBadRequestNoAlias() {
var requestWithoutAlias = provideIdentityProviderRequestCreate();
requestWithoutAlias.setAlias(null);
var e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.createIdentityProvider(requestWithoutAlias));
assertEquals(400, e.status());
var descriptorRequestWithoutAlias = provideIdentityProviderWithDescriptorRequest();
descriptorRequestWithoutAlias.setAlias(null);
e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.createIdentityProviderFromDescriptor(descriptorRequestWithoutAlias));
assertEquals(400, e.status());
e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.updateIdentityProvider(null, provideIdentityProviderRequestUpdate()));
assertEquals(404, e.status());
}
@Test
public void testCreateEntityProviderBadRequestNoProvider() {
var requestWithoutProvider = provideIdentityProviderRequestCreate();
requestWithoutProvider.setProviderId(null);
var e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.createIdentityProvider(requestWithoutProvider));
assertEquals(400, e.status());
var descriptorRequestWithoutProvider = provideIdentityProviderWithDescriptorRequest();
descriptorRequestWithoutProvider.setProviderId(null);
e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.createIdentityProviderFromDescriptor(descriptorRequestWithoutProvider));
assertEquals(400, e.status());
var updateRequestWithoutProvider = provideIdentityProviderRequestUpdate();
updateRequestWithoutProvider.setProviderId(null);
e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.updateIdentityProvider(idp1TestAlias, updateRequestWithoutProvider));
assertEquals(400, e.status());
}
@Test
public void testCreateEntityProviderBadRequestNoEntityId() {
var requestWithoutEntityId = provideIdentityProviderRequestCreate();
requestWithoutEntityId.getConfig().setEntityId(null);
var e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.createIdentityProvider(requestWithoutEntityId));
assertEquals(400, e.status());
var descriptorRequestWithoutEntityId = provideIdentityProviderWithDescriptorRequest();
descriptorRequestWithoutEntityId.setEntityId(null);
e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.createIdentityProviderFromDescriptor(descriptorRequestWithoutEntityId));
assertEquals(400, e.status());
var updateRequestWithoutEntityId = provideIdentityProviderRequestUpdate();
updateRequestWithoutEntityId.getConfig().setEntityId(null);
e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.updateIdentityProvider(testAlias, updateRequestWithoutEntityId));
assertEquals(400, e.status());
}
@Test
public void testCreateEntityProviderBadRequestNoIdpEntityId() {
var requestWithoutIdpEntityId = provideIdentityProviderRequestCreate();
requestWithoutIdpEntityId.getConfig().setIdpEntityId(null);
var e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.createIdentityProvider(requestWithoutIdpEntityId));
assertEquals(400, e.status());
var updateRequestWithoutEntityId = provideIdentityProviderRequestUpdate();
updateRequestWithoutEntityId.getConfig().setIdpEntityId(null);
e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.updateIdentityProvider(testAlias, updateRequestWithoutEntityId));
assertEquals(400, e.status());
}
@Test
public void testCreateEntityProviderBadRequestNoSSOUrl() {
var requestWithoutSSOUrl = provideIdentityProviderRequestCreate();
requestWithoutSSOUrl.getConfig().setSingleSignOnServiceUrl(null);
var e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.createIdentityProvider(requestWithoutSSOUrl));
assertEquals(400, e.status());
var updateRequestWithoutSSOUrl = provideIdentityProviderRequestUpdate();
updateRequestWithoutSSOUrl.getConfig().setSingleSignOnServiceUrl(null);
e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.updateIdentityProvider(idp1TestAlias, updateRequestWithoutSSOUrl));
assertEquals(400, e.status());
}
@Test
public void testCreateEntityProviderBadRequestMalformedSSOUrl() {
var requestWithMalformedSingleSignOnURL = provideIdentityProviderRequestCreate();
requestWithMalformedSingleSignOnURL.setAlias("andAnotherAlias");
requestWithMalformedSingleSignOnURL.getConfig().setSingleSignOnServiceUrl("this is no valid URL :(");
e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.createIdentityProvider(requestWithMalformedSingleSignOnURL));
requestWithMalformedSingleSignOnURL.getConfig().setSingleSignOnServiceUrl(invalidUrl);
var e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.createIdentityProvider(requestWithMalformedSingleSignOnURL));
assertEquals(400, e.status());
var updateRequestWithMalformedSingleSignOnURL = provideIdentityProviderRequestUpdate();
updateRequestWithMalformedSingleSignOnURL.getConfig().setSingleSignOnServiceUrl(invalidUrl);
e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.updateIdentityProvider(idp1TestAlias, updateRequestWithMalformedSingleSignOnURL));
assertEquals(400, e.status());
}
@Test
public void testCreateEntityProviderBadRequestMalformedEntityDescriptorUrl() {
var descriptorRequestWithMalformedEntityDescriptorUrl = provideIdentityProviderWithDescriptorRequest();
descriptorRequestWithMalformedEntityDescriptorUrl.setSamlEntityDescriptorURL(invalidUrl);
var e = assertThrows(FeignException.class,
() -> identityProviderConfigurationClient.createIdentityProviderFromDescriptor(descriptorRequestWithMalformedEntityDescriptorUrl));
assertEquals(400, e.status());
}
@Test
public void testUpdateEntityProviderInvalidOperation() {
var invalidUpdateRequest = provideIdentityProviderRequestUpdate();
invalidUpdateRequest.getConfig().setPrincipalType(IdentityProviderPrincipalType.SUBJECT);
var e = assertThrows(FeignException.class,
() -> identityProviderConfigurationClient.updateIdentityProvider(idp1TestAlias, invalidUpdateRequest));
assertEquals(400, e.status());
}
@ -132,6 +313,35 @@ public class IdentityProviderConfigurationTest extends AbstractTenantUserManagem
.build();
}
private IdentityProviderRequest provideIdentityProviderRequestCreateForIdp1() {
return IdentityProviderRequest.builder()
.alias(idp1TestAlias)
.config(IdentityProviderConfigRequest.builder()
.entityId(entityId)
.idpEntityId(idpEntityId)
.singleSignOnServiceUrl(ssoServiceUrl)
.nameIDPolicyFormat(IdentityProviderNameIDPolicyFormat.TRANSIENT)
.principalType(IdentityProviderPrincipalType.ATTRIBUTE)
.build())
.build();
}
private IdentityProviderRequest provideIdentityProviderRequestCreateForIdp2() {
return IdentityProviderRequest.builder()
.alias(idp2TestAlias)
.config(IdentityProviderConfigRequest.builder()
.entityId(entityId)
.idpEntityId(idpEntityId)
.singleSignOnServiceUrl(ssoServiceUrl)
.nameIDPolicyFormat(IdentityProviderNameIDPolicyFormat.X_509_SUBJECT_NAME)
.principalType(IdentityProviderPrincipalType.SUBJECT)
.build())
.build();
}
private IdentityProviderRequest provideIdentityProviderRequestUpdate() {
return IdentityProviderRequest.builder()