diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/api/external/IdentityProviderConfigurationResource.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/api/external/IdentityProviderConfigurationResource.java index fdbb59a..2455e3b 100644 --- a/src/main/java/com/knecon/fforesight/tenantusermanagement/api/external/IdentityProviderConfigurationResource.java +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/api/external/IdentityProviderConfigurationResource.java @@ -23,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; @Tag(name = "Identity provider configuration endpoints", description = "Provides operations related to the identity providers") @ApiResponses(value = {@ApiResponse(responseCode = "429", description = "Too many requests."), @ApiResponse(responseCode = "403", description = "Forbidden")}) @@ -56,7 +57,7 @@ public interface IdentityProviderConfigurationResource { @PostMapping(value = API_PATH, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Creates a new identity provider", description = "None") @ApiResponses(value = {@ApiResponse(responseCode = "201", description = "Successfully created the identity provider"), @ApiResponse(responseCode = "400", description = "Malformed request parameters or body"), @ApiResponse(responseCode = "409", description = "Duplicate")}) - ResponseEntity createIdentityProvider(@io.swagger.v3.oas.annotations.parameters.RequestBody @RequestBody IdentityProviderRequest identityProvider); + ResponseEntity createIdentityProvider(@Valid @io.swagger.v3.oas.annotations.parameters.RequestBody @RequestBody IdentityProviderRequest identityProvider); @ResponseStatus(value = HttpStatus.CREATED) @@ -64,7 +65,7 @@ public interface IdentityProviderConfigurationResource { @PostMapping(value = API_PATH + IMPORT_SUB_PATH, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Creates a new identity provider", description = "None") @ApiResponses(value = {@ApiResponse(responseCode = "201", description = "Successfully created the identity provider"), @ApiResponse(responseCode = "400", description = "Malformed request parameters or body"), @ApiResponse(responseCode = "409", description = "Duplicate")}) - ResponseEntity createIdentityProviderFromDescriptor(@io.swagger.v3.oas.annotations.parameters.RequestBody @RequestBody IdentityProviderWithDescriptorRequest identityProvider); + ResponseEntity createIdentityProviderFromDescriptor(@Valid @io.swagger.v3.oas.annotations.parameters.RequestBody @RequestBody IdentityProviderWithDescriptorRequest identityProvider); @ResponseStatus(value = HttpStatus.OK) @@ -73,7 +74,7 @@ public interface IdentityProviderConfigurationResource { @Operation(summary = "Updates an existing identity provider", description = "None") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Successfully updated the identity provider"), @ApiResponse(responseCode = "404", description = "Not found"), @ApiResponse(responseCode = "400", description = "Malformed request parameters or body")}) ResponseEntity updateIdentityProvider(@Parameter(name = IDENTITY_PROVIDER_ALIAS_PARAM, description = "The alias of the identity provider to retrieve.", required = true) @PathVariable(IDENTITY_PROVIDER_ALIAS_PARAM) String identityProviderAlias, - @io.swagger.v3.oas.annotations.parameters.RequestBody @RequestBody IdentityProviderRequest identityProvider); + @Valid @io.swagger.v3.oas.annotations.parameters.RequestBody @RequestBody IdentityProviderRequest identityProvider); @ResponseStatus(value = HttpStatus.NO_CONTENT) diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/controller/ControllerAdvice.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/controller/ControllerAdvice.java index ad09260..176f131 100644 --- a/src/main/java/com/knecon/fforesight/tenantusermanagement/controller/ControllerAdvice.java +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/controller/ControllerAdvice.java @@ -1,5 +1,7 @@ package com.knecon.fforesight.tenantusermanagement.controller; +import java.util.stream.Collectors; + import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -23,7 +25,9 @@ public class ControllerAdvice { @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { - return new ResponseEntity<>(new ErrorMessage(e.getMessage()), HttpStatus.BAD_REQUEST); + var errorList = e.getFieldErrors(); + String errorListAsString = errorList.stream().map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage()).collect(Collectors.joining(", ")); + return new ResponseEntity<>(new ErrorMessage(String.format("You have empty/wrong formatted parameters: %s", errorListAsString)), HttpStatus.BAD_REQUEST); } diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/controller/external/IdentityProviderConfigurationController.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/controller/external/IdentityProviderConfigurationController.java index 63d3db0..6a56b7e 100644 --- a/src/main/java/com/knecon/fforesight/tenantusermanagement/controller/external/IdentityProviderConfigurationController.java +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/controller/external/IdentityProviderConfigurationController.java @@ -25,6 +25,7 @@ import org.springframework.web.server.ResponseStatusException; import com.knecon.fforesight.tenantcommons.TenantContext; import com.knecon.fforesight.tenantusermanagement.api.external.IdentityProviderConfigurationResource; import com.knecon.fforesight.tenantusermanagement.api.external.PublicResource; +import com.knecon.fforesight.tenantusermanagement.exception.ConflictException; import com.knecon.fforesight.tenantusermanagement.exception.IdentityProviderExistsAlreadyException; import com.knecon.fforesight.tenantusermanagement.exception.IdentityProviderNotFoundException; import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderList; @@ -82,28 +83,43 @@ public class IdentityProviderConfigurationController implements IdentityProvider @Override @PreAuthorize("hasAuthority('" + WRITE_IDENTITY_PROVIDER_CONFIGURATION + "')") - public ResponseEntity createIdentityProvider(IdentityProviderRequest identityProvider) { + public ResponseEntity createIdentityProvider(IdentityProviderRequest identityProviderRequest) { - IdentityProviderModel identityProviderModel = IdentityProviderMappingService.toModelFromRequest(identityProvider); + if (identityProviderRequest.getAlias() == null) { + throw new BadRequestException("Alias must not be null."); + } + + if(identityProviderRequest.getDisplayName().isEmpty()) { + identityProviderRequest.setDisplayName(identityProviderRequest.getAlias()); + } + throwConflictExceptionIfDisplayNameAlreadyExists(identityProviderRequest.getDisplayName()); + + IdentityProviderModel identityProviderModel = IdentityProviderMappingService.toModelFromRequest(identityProviderRequest); return callKeyCloakIdentityProvidersCreateForModel(identityProviderModel); } @Override @PreAuthorize("hasAuthority('" + WRITE_IDENTITY_PROVIDER_CONFIGURATION + "')") - public ResponseEntity createIdentityProviderFromDescriptor(IdentityProviderWithDescriptorRequest identityProvider) { + public ResponseEntity createIdentityProviderFromDescriptor(IdentityProviderWithDescriptorRequest identityProviderWithDescriptorRequest) { + + if(identityProviderWithDescriptorRequest.getDisplayName().isEmpty()) { + identityProviderWithDescriptorRequest.setDisplayName(identityProviderWithDescriptorRequest.getAlias()); + } + throwConflictExceptionIfDisplayNameAlreadyExists(identityProviderWithDescriptorRequest.getDisplayName()); Map requestMap = new HashMap<>(); - requestMap.put("providerId", identityProvider.getProviderId()); - requestMap.put("fromUrl", identityProvider.getSamlEntityDescriptorURL()); + requestMap.put("providerId", identityProviderWithDescriptorRequest.getProviderId()); + requestMap.put("fromUrl", identityProviderWithDescriptorRequest.getSamlEntityDescriptorURL()); Map 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", identityProvider.getEntityId()); - IdentityProviderModel identityProviderModel = IdentityProviderMappingService.toModelFromDescriptorRequestAndConfig(identityProvider, configurationsMap); + configurationsMap.put("entityId", identityProviderWithDescriptorRequest.getEntityId()); + IdentityProviderModel identityProviderModel = IdentityProviderMappingService.toModelFromDescriptorRequestAndConfig(identityProviderWithDescriptorRequest, + configurationsMap); return callKeyCloakIdentityProvidersCreateForModel(identityProviderModel); } @@ -120,13 +136,7 @@ public class IdentityProviderConfigurationController implements IdentityProvider return new ResponseEntity<>(createdIdentityProvider, HttpStatus.valueOf(response.getStatus())); } case CONFLICT -> throw new IdentityProviderExistsAlreadyException(identityProviderModel.getAlias()); - default -> { - if (httpStatus.is4xxClientError()) { - throw new ResponseStatusException(httpStatus, "Bad request to keycloak API"); - } else { - throw new ResponseStatusException(httpStatus, httpStatus.getReasonPhrase()); - } - } + default -> throw new ResponseStatusException(httpStatus, extractKeycloakErrorMessageInfos(response.readEntity(String.class))); } } } @@ -134,15 +144,15 @@ public class IdentityProviderConfigurationController implements IdentityProvider @Override @PreAuthorize("hasAuthority('" + WRITE_IDENTITY_PROVIDER_CONFIGURATION + "')") - public ResponseEntity updateIdentityProvider(String identityProviderAlias, IdentityProviderRequest identityProvider) { + public ResponseEntity updateIdentityProvider(String identityProviderAlias, IdentityProviderRequest identityProviderRequest) { - identityProvider.setAlias(identityProviderAlias); + identityProviderRequest.setAlias(identityProviderAlias); getIdentityProviderRepresentation(identityProviderAlias); - IdentityProviderModel identityProviderModel = IdentityProviderMappingService.toModelFromRequest(identityProvider); + IdentityProviderModel identityProviderModel = IdentityProviderMappingService.toModelFromRequest(identityProviderRequest); identityProviderModel.setSpecificDefaults(); var restTemplate = new RestTemplate(); - var url = getKeycloakIdentityProviderInstancesUrl() + identityProvider.getAlias(); + var url = getKeycloakIdentityProviderInstancesUrl() + identityProviderRequest.getAlias(); var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.add("Authorization", "Bearer " + getToken()); @@ -151,7 +161,7 @@ public class IdentityProviderConfigurationController implements IdentityProvider ResponseEntity response = restTemplate.exchange(url, HttpMethod.PUT, httpEntity, String.class); var httpStatus = HttpStatus.valueOf(response.getStatusCode().value()); if (httpStatus == HttpStatus.NO_CONTENT) { - IdentityProviderModel createdIdentityProvider = getIdentityProvider(identityProvider.getAlias()); + IdentityProviderModel createdIdentityProvider = getIdentityProvider(identityProviderRequest.getAlias()); return new ResponseEntity<>(createdIdentityProvider, HttpStatus.OK); } else if (httpStatus.is4xxClientError()) { throw new ResponseStatusException(httpStatus, "Bad request to keycloak API"); @@ -184,6 +194,16 @@ public class IdentityProviderConfigurationController implements IdentityProvider } + private void throwConflictExceptionIfDisplayNameAlreadyExists(String displayName) { + + if (getIdentityProviders().getIdentityProviders() + .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."); + } + } + + private static String extractKeycloakErrorMessageInfos(String keyCloakErrorMessage) { String errorMessageRegex = "\\{\"errorMessage\":\"(.*?)\"}"; diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderConfigRequest.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderConfigRequest.java index 9a1b211..80f4611 100644 --- a/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderConfigRequest.java +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderConfigRequest.java @@ -1,6 +1,7 @@ package com.knecon.fforesight.tenantusermanagement.model; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -22,12 +23,15 @@ public class IdentityProviderConfigRequest { private Integer guiOrder = 0; @Schema(description = "Service provider entity ID : It is used to uniquely identify this SAML Service Provider.", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull private String entityId; @Schema(description = "Identity provider entity ID : It is used to validate the Issuer for received SAML assertions. If empty, no Issuer validation is performed.", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull private String idpEntityId; @Schema(description = "The Url that must be used to send authentication requests (SAML AuthnRequest).", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull private String singleSignOnServiceUrl; @Builder.Default diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderRequest.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderRequest.java index 9213109..8c775c6 100644 --- a/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderRequest.java +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderRequest.java @@ -1,6 +1,7 @@ package com.knecon.fforesight.tenantusermanagement.model; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -18,6 +19,7 @@ public class IdentityProviderRequest { @Builder.Default @Schema(description = "Configuration of the identity provider", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull private IdentityProviderConfigRequest config = new IdentityProviderConfigRequest(); @Builder.Default diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderWithDescriptorRequest.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderWithDescriptorRequest.java index 54ab54b..2a4d5e1 100644 --- a/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderWithDescriptorRequest.java +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderWithDescriptorRequest.java @@ -1,6 +1,7 @@ package com.knecon.fforesight.tenantusermanagement.model; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -14,6 +15,7 @@ import lombok.NoArgsConstructor; public class IdentityProviderWithDescriptorRequest { @Schema(description = "Alias of the identity provider used for identification", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull private String alias; @Builder.Default @@ -25,9 +27,10 @@ public class IdentityProviderWithDescriptorRequest { private String providerId = "saml"; @Schema(description = "The SAML entity descriptor as a URL to external IDP metadata, used to fill other fields for a faster identity provider creation process", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull private String samlEntityDescriptorURL; @Schema(description = "Service provider entity ID that will be used to uniquely identify this SAML Service Provider", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull private String entityId; - } diff --git a/src/test/java/com/knecon/fforesight/tenantusermanagement/tests/IdentityProviderConfigurationTest.java b/src/test/java/com/knecon/fforesight/tenantusermanagement/tests/IdentityProviderConfigurationTest.java index becec5b..ab00e63 100644 --- a/src/test/java/com/knecon/fforesight/tenantusermanagement/tests/IdentityProviderConfigurationTest.java +++ b/src/test/java/com/knecon/fforesight/tenantusermanagement/tests/IdentityProviderConfigurationTest.java @@ -97,6 +97,24 @@ public class IdentityProviderConfigurationTest extends AbstractTenantUserManagem 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); + e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.createIdentityProvider(requestWithSameDisplayName)); + assertEquals(409, e.status()); + + var requestWithMalformedSingleSignOnURL = provideIdentityProviderRequestCreate(); + requestWithMalformedSingleSignOnURL.setAlias("andAnotherAlias"); + requestWithMalformedSingleSignOnURL.getConfig().setSingleSignOnServiceUrl("this is no valid URL :("); + e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.createIdentityProvider(requestWithMalformedSingleSignOnURL)); + assertEquals(400, e.status()); + }