From 5af289479e7c146e1c10a4dfdecca4a4406eadad Mon Sep 17 00:00:00 2001 From: Maverick Studer Date: Tue, 13 Feb 2024 12:42:21 +0100 Subject: [PATCH] RED-8477: SSO settings endpoint for SAML --- build.gradle.kts | 1 + ...IdentityProviderConfigurationResource.java | 85 +++++++ .../api/external/TenantsResource.java | 18 +- .../controller/ControllerAdvice.java | 7 +- ...entityProviderConfigurationController.java | 223 ++++++++++++++++++ .../exception/ConflictException.java | 23 ++ ...dentityProviderExistsAlreadyException.java | 14 ++ .../IdentityProviderNotFoundException.java | 16 ++ .../model/ExtensibleModel.java | 10 + .../model/IdentityProviderConfigModel.java | 128 ++++++++++ .../model/IdentityProviderConfigRequest.java | 62 +++++ .../model/IdentityProviderList.java | 19 ++ .../model/IdentityProviderModel.java | 69 ++++++ .../IdentityProviderNameIDPolicyFormat.java | 43 ++++ .../model/IdentityProviderPrincipalType.java | 24 ++ .../model/IdentityProviderRequest.java | 31 +++ .../IdentityProviderSAMLSignatureKeyName.java | 7 + .../IdentityProviderSignatureAlgorithm.java | 10 + .../model/IdentityProviderSyncMode.java | 8 + ...IdentityProviderWithDescriptorRequest.java | 33 +++ .../UserManagementPermissions.java | 4 + .../utils/IdentityProviderMappingService.java | 179 ++++++++++++++ src/main/resources/application-clarifynd.yaml | 4 + src/main/resources/application-dev.yaml | 3 + src/main/resources/application-documine.yaml | 6 +- src/main/resources/application-redaction.yaml | 8 +- .../IdentityProviderConfigurationClient.java | 10 + .../IdentityProviderConfigurationTest.java | 144 +++++++++++ src/test/resources/application.yaml | 4 + 29 files changed, 1177 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/api/external/IdentityProviderConfigurationResource.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/controller/external/IdentityProviderConfigurationController.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/exception/ConflictException.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/exception/IdentityProviderExistsAlreadyException.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/exception/IdentityProviderNotFoundException.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/model/ExtensibleModel.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderConfigModel.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderConfigRequest.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderList.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderModel.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderNameIDPolicyFormat.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderPrincipalType.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderRequest.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderSAMLSignatureKeyName.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderSignatureAlgorithm.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderSyncMode.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderWithDescriptorRequest.java create mode 100644 src/main/java/com/knecon/fforesight/tenantusermanagement/utils/IdentityProviderMappingService.java create mode 100644 src/test/java/com/knecon/fforesight/tenantusermanagement/feigntestclients/external/IdentityProviderConfigurationClient.java create mode 100644 src/test/java/com/knecon/fforesight/tenantusermanagement/tests/IdentityProviderConfigurationTest.java diff --git a/build.gradle.kts b/build.gradle.kts index 61cdc08..11d73ea 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -96,6 +96,7 @@ configurations { dependencies { + implementation("com.knecon.fforesight:database-tenant-commons:0.21.0") implementation("com.knecon.fforesight:keycloak-commons:0.25.0") implementation("com.knecon.fforesight:swagger-commons:0.5.0") implementation("com.knecon.fforesight:tracing-commons:0.5.0") 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 new file mode 100644 index 0000000..29e439c --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/api/external/IdentityProviderConfigurationResource.java @@ -0,0 +1,85 @@ +package com.knecon.fforesight.tenantusermanagement.api.external; + + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderList; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderModel; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderRequest; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderWithDescriptorRequest; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +@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")}) +public interface IdentityProviderConfigurationResource { + + String API_PATH = "/configuration/identity-providers"; + String IDENTITY_PROVIDER_ALIAS_PARAM = "providerAlias"; + String IDENTITY_PROVIDER_ALIAS_PATH_PARAM = "/{" + IDENTITY_PROVIDER_ALIAS_PARAM + "}"; + String IMPORT_SUB_PATH = "/import"; + + + @ResponseStatus(value = HttpStatus.OK) + @ResponseBody + @GetMapping(value = API_PATH, produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Gets all existing identity providers.", description = "None") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) + IdentityProviderList getIdentityProviders(); + + + @ResponseStatus(value = HttpStatus.OK) + @ResponseBody + @GetMapping(value = API_PATH + IDENTITY_PROVIDER_ALIAS_PATH_PARAM, produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Gets an existing identity provider.", description = "None") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "404", description = "Not found")}) + IdentityProviderModel getIdentityProvider(@Parameter(name = IDENTITY_PROVIDER_ALIAS_PARAM, description = "The alias of the identity provider to retrieve.", required = true) @PathVariable(IDENTITY_PROVIDER_ALIAS_PARAM) String identityProviderAlias); + + + + @ResponseStatus(value = HttpStatus.CREATED) + @ResponseBody + @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(@RequestBody IdentityProviderRequest identityProvider); + + + @ResponseStatus(value = HttpStatus.CREATED) + @ResponseBody + @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(@RequestBody IdentityProviderWithDescriptorRequest identityProvider); + + + @ResponseStatus(value = HttpStatus.OK) + @ResponseBody + @PutMapping(value = API_PATH + IDENTITY_PROVIDER_ALIAS_PATH_PARAM, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @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, + @RequestBody IdentityProviderRequest identityProvider); + + + @ResponseStatus(value = HttpStatus.NO_CONTENT) + @DeleteMapping(value = API_PATH + IDENTITY_PROVIDER_ALIAS_PATH_PARAM) + @Operation(summary = "Deletes an existing identity provider.", description = "None") + @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Successfully deleted the identity provider."), @ApiResponse(responseCode = "404", description = "Not found")}) + void deleteIdentityProvider(@Parameter(name = IDENTITY_PROVIDER_ALIAS_PARAM, description = "The alias of the identity provider to retrieve.", required = true) @PathVariable(IDENTITY_PROVIDER_ALIAS_PARAM) String identityProviderAlias); + +} diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/api/external/TenantsResource.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/api/external/TenantsResource.java index d304721..23ee2aa 100644 --- a/src/main/java/com/knecon/fforesight/tenantusermanagement/api/external/TenantsResource.java +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/api/external/TenantsResource.java @@ -27,11 +27,13 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; @ResponseStatus(value = HttpStatus.OK) public interface TenantsResource { + String TENANTS_PATH = "/tenants"; String TENANT_ID_PARAM = "tenantId"; String TENANT_ID_PATH_PARAM = "/{" + TENANT_ID_PARAM + "}"; + String TENANTS_TENANT_ID_PATH = TENANTS_PATH + TENANT_ID_PATH_PARAM; - @PostMapping(value = "/tenants", consumes = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = TENANTS_PATH, consumes = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Create a new tenant", description = "None") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) void createTenant(@RequestBody TenantRequest tenant); @@ -42,35 +44,35 @@ public interface TenantsResource { @Operation(summary = "Deletes given tenant", description = "None") @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "OK"), @ApiResponse(responseCode = "403", description = "Forbidden access, you dont have rights to delete tenants"), @ApiResponse(responseCode = "405", description = "Operation is not allowed."), @ApiResponse(responseCode = "409", description = "Conflict while deleting tenant.")}) - @DeleteMapping(value = "/tenants/{tenantId}") + @DeleteMapping(value = TENANTS_TENANT_ID_PATH) void deleteTenant(@PathVariable("tenantId") String tenantId); - @GetMapping(value = "/tenants", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = TENANTS_PATH, produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Gets all existing tenants", description = "None") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) List getTenants(); - @GetMapping(value = "/tenants/{tenantId}", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = TENANTS_TENANT_ID_PATH, produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Gets an existing tenant", description = "None") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) TenantResponse getTenant(@PathVariable("tenantId") String tenantId); - @PutMapping(value = "/tenants/{tenantId}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @PutMapping(value = TENANTS_TENANT_ID_PATH, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Update existing tenant", description = "None") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) TenantResponse updateTenant(@PathVariable("tenantId") String tenantId, @RequestBody TenantRequest tenantRequest); - @PostMapping(value = "/tenants/{tenantId}/details", consumes = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = TENANTS_TENANT_ID_PATH + "/details", consumes = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Update details", description = "None") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) void updateDetails(@PathVariable("tenantId") String tenantId, @RequestBody UpdateDetailsRequest request); - @GetMapping(value = "/tenants/simple", produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(value = TENANTS_PATH + "/simple", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Gets all existing tenants in a simplified format", description = "None") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) List getSimpleTenants(); @@ -82,7 +84,7 @@ public interface TenantsResource { DeploymentKeyResponse getDeploymentKey(@PathVariable(TENANT_ID_PARAM) String tenantId); - @PostMapping(value = "/tenants/{tenantId}/sync", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = TENANTS_TENANT_ID_PATH + "/sync", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Sync existing tenant", description = "None") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) void syncTenant(@PathVariable("tenantId") String tenantId, @RequestBody JsonNode payload); 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 62a23fa..fdca2de 100644 --- a/src/main/java/com/knecon/fforesight/tenantusermanagement/controller/ControllerAdvice.java +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/controller/ControllerAdvice.java @@ -1,6 +1,5 @@ package com.knecon.fforesight.tenantusermanagement.controller; - import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; @@ -10,6 +9,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.server.ResponseStatusException; import com.knecon.fforesight.tenantusermanagement.model.ErrorMessage; + import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.NotFoundException; @@ -23,13 +23,17 @@ public class ControllerAdvice { return new ResponseEntity<>(new ErrorMessage(e.getMessage()), HttpStatus.BAD_REQUEST); } + @ExceptionHandler(NotFoundException.class) public ResponseEntity handleNotFound(NotFoundException e) { + return new ResponseEntity<>(new ErrorMessage(e.getMessage()), HttpStatus.NOT_FOUND); } + @ExceptionHandler(ForbiddenException.class) public ResponseEntity handleForbiddenAccess(ForbiddenException e) { + return new ResponseEntity<>(new ErrorMessage(e.getMessage()), HttpStatus.FORBIDDEN); } @@ -39,6 +43,7 @@ public class ControllerAdvice { return new ResponseEntity<>(new ErrorMessage(e.getReason()), e.getStatusCode()); } + @ExceptionHandler(BadRequestException.class) public ResponseEntity handleBadRequestException(BadRequestException e) { 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 new file mode 100644 index 0000000..eabde35 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/controller/external/IdentityProviderConfigurationController.java @@ -0,0 +1,223 @@ +package com.knecon.fforesight.tenantusermanagement.controller.external; + +import static com.knecon.fforesight.tenantusermanagement.permissions.UserManagementPermissions.READ_IDENTITY_PROVIDER_CONFIGURATION; +import static com.knecon.fforesight.tenantusermanagement.permissions.UserManagementPermissions.WRITE_IDENTITY_PROVIDER_CONFIGURATION; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; +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.IdentityProviderExistsAlreadyException; +import com.knecon.fforesight.tenantusermanagement.exception.IdentityProviderNotFoundException; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderList; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderModel; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderRequest; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderWithDescriptorRequest; +import com.knecon.fforesight.tenantusermanagement.properties.TenantUserManagementProperties; +import com.knecon.fforesight.tenantusermanagement.service.KeyCloakAdminClientService; +import com.knecon.fforesight.tenantusermanagement.service.RealmService; +import com.knecon.fforesight.tenantusermanagement.utils.IdentityProviderMappingService; + +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.core.Response; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestController +@RequiredArgsConstructor +public class IdentityProviderConfigurationController implements IdentityProviderConfigurationResource, PublicResource { + + private final RealmService realmService; + private final TenantUserManagementProperties tenantUserManagementProperties; + private final KeyCloakAdminClientService keyCloakAdminClientService; + + + @Override + @PreAuthorize("hasAuthority('" + READ_IDENTITY_PROVIDER_CONFIGURATION + "')") + public IdentityProviderList getIdentityProviders() { + + return new IdentityProviderList(getRealmRepresentation().getIdentityProviders() + .stream() + .map(IdentityProviderMappingService::toModelFromRepresentation) + .toList()); + } + + + @Override + @PreAuthorize("hasAuthority('" + READ_IDENTITY_PROVIDER_CONFIGURATION + "')") + public IdentityProviderModel getIdentityProvider(String identityProviderAlias) { + + return IdentityProviderMappingService.toModelFromRepresentation(getIdentityProviderRepresentation(identityProviderAlias)); + } + + + private IdentityProviderRepresentation getIdentityProviderRepresentation(String identityProviderAlias) { + + return getRealmRepresentation().getIdentityProviders() + .stream() + .filter(identityProviderRepresentation -> identityProviderRepresentation.getAlias().equals(identityProviderAlias)) + .findFirst() + .orElseThrow(() -> new IdentityProviderNotFoundException(identityProviderAlias)); + } + + + @Override + @PreAuthorize("hasAuthority('" + WRITE_IDENTITY_PROVIDER_CONFIGURATION + "')") + public ResponseEntity createIdentityProvider(IdentityProviderRequest identityProvider) { + + IdentityProviderModel identityProviderModel = IdentityProviderMappingService.toModelFromRequest(identityProvider); + return callKeyCloakIdentityProvidersCreateForModel(identityProviderModel); + } + + + @Override + @PreAuthorize("hasAuthority('" + WRITE_IDENTITY_PROVIDER_CONFIGURATION + "')") + public ResponseEntity createIdentityProviderFromDescriptor(IdentityProviderWithDescriptorRequest identityProvider) { + + Map requestMap = new HashMap<>(); + requestMap.put("providerId", identityProvider.getProviderId()); + requestMap.put("fromUrl", identityProvider.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); + return callKeyCloakIdentityProvidersCreateForModel(identityProviderModel); + } + + + private ResponseEntity callKeyCloakIdentityProvidersCreateForModel(IdentityProviderModel identityProviderModel) { + + identityProviderModel.setSpecificDefaults(); + + try (Response response = realmService.realm(getTenantId()).identityProviders().create(IdentityProviderMappingService.toRepresentationFromModel(identityProviderModel))) { + var httpStatus = HttpStatus.valueOf(response.getStatus()); + switch (httpStatus) { + case CREATED -> { + IdentityProviderModel createdIdentityProvider = getIdentityProvider(identityProviderModel.getAlias()); + return new ResponseEntity<>(createdIdentityProvider, HttpStatus.valueOf(response.getStatus())); + } + case CONFLICT -> throw new IdentityProviderExistsAlreadyException(response.getStatusInfo().getReasonPhrase()); + default -> { + if (httpStatus.is4xxClientError()) { + throw new ResponseStatusException(httpStatus, "Bad request to keycloak API"); + } else { + throw new ResponseStatusException(httpStatus); + } + } + } + } + } + + + @Override + @PreAuthorize("hasAuthority('" + WRITE_IDENTITY_PROVIDER_CONFIGURATION + "')") + public ResponseEntity updateIdentityProvider(String identityProviderAlias, IdentityProviderRequest identityProvider) { + + identityProvider.setAlias(identityProviderAlias); + getIdentityProviderRepresentation(identityProviderAlias); + IdentityProviderModel identityProviderModel = IdentityProviderMappingService.toModelFromRequest(identityProvider); + identityProviderModel.setSpecificDefaults(); + + var restTemplate = new RestTemplate(); + var url = getKeycloakIdentityProviderInstancesUrl() + identityProvider.getAlias(); + var headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.add("Authorization", "Bearer " + getToken()); + HttpEntity httpEntity = new HttpEntity<>(identityProviderModel, headers); + try { + 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()); + return new ResponseEntity<>(createdIdentityProvider, HttpStatus.OK); + } else if (httpStatus.is4xxClientError()) { + throw new ResponseStatusException(httpStatus, "Bad request to keycloak API"); + } else { + throw new ResponseStatusException(httpStatus); + } + } catch (HttpClientErrorException e) { + throw new ResponseStatusException(e.getStatusCode(), extractKeycloakErrorMessageInfos(e.getMessage())); + } + } + + + @Override + @PreAuthorize("hasAuthority('" + WRITE_IDENTITY_PROVIDER_CONFIGURATION + "')") + public void deleteIdentityProvider(String identityProviderAlias) { + + getIdentityProviderRepresentation(identityProviderAlias); + + var restTemplate = new RestTemplate(); + var url = getKeycloakIdentityProviderInstancesUrl() + identityProviderAlias; + var headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.add("Authorization", "Bearer " + getToken()); + HttpEntity httpEntity = new HttpEntity<>(headers); + try { + restTemplate.exchange(url, HttpMethod.DELETE, httpEntity, String.class); + } catch (HttpClientErrorException e) { + throw new ResponseStatusException(e.getStatusCode(), extractKeycloakErrorMessageInfos(e.getMessage())); + } + } + + + private static String extractKeycloakErrorMessageInfos(String keyCloakErrorMessage) { + + String errorMessageRegex = "\\{\"errorMessage\":\"(.*?)\"}"; + Pattern pattern = Pattern.compile(errorMessageRegex); + Matcher matcher = pattern.matcher(keyCloakErrorMessage); + + if (matcher.find()) { + return matcher.group(1); + } + return keyCloakErrorMessage; + } + + + private String getTenantId() { + + return TenantContext.getTenantId(); + } + + + private RealmRepresentation getRealmRepresentation() { + + return realmService.realm(getTenantId()).toRepresentation(); + } + + + private String getKeycloakIdentityProviderInstancesUrl() { + + return tenantUserManagementProperties.getServerUrl() + "/admin/realms/" + getTenantId() + "/identity-provider/instances/"; + } + + + private String getToken() { + + return keyCloakAdminClientService.getAdminClient().tokenManager().getAccessToken().getToken(); + } + +} diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/exception/ConflictException.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/exception/ConflictException.java new file mode 100644 index 0000000..af948f1 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/exception/ConflictException.java @@ -0,0 +1,23 @@ +package com.knecon.fforesight.tenantusermanagement.exception; + +public class ConflictException extends RuntimeException { + + public ConflictException(String message) { + + super(message); + } + + + public ConflictException(String message, Throwable t) { + + super(message, t); + } + + + public static ConflictException withObjectName(String objectName) { + + return new ConflictException(String.format("An object of type %s already exists.", objectName)); + + } + +} diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/exception/IdentityProviderExistsAlreadyException.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/exception/IdentityProviderExistsAlreadyException.java new file mode 100644 index 0000000..ed95c51 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/exception/IdentityProviderExistsAlreadyException.java @@ -0,0 +1,14 @@ +package com.knecon.fforesight.tenantusermanagement.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.CONFLICT) +public class IdentityProviderExistsAlreadyException extends ConflictException { + + private static final String IDENTITY_PROVIDER_ALREADY_EXISTS_MESSAGE = "Identity provider with alias %s already exists in keycloak."; + + public IdentityProviderExistsAlreadyException(String identityProviderAlias) { + super(String.format(IDENTITY_PROVIDER_ALREADY_EXISTS_MESSAGE, identityProviderAlias)); + } +} diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/exception/IdentityProviderNotFoundException.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/exception/IdentityProviderNotFoundException.java new file mode 100644 index 0000000..bf6c562 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/exception/IdentityProviderNotFoundException.java @@ -0,0 +1,16 @@ +package com.knecon.fforesight.tenantusermanagement.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +import jakarta.ws.rs.NotFoundException; + +@ResponseStatus(code = HttpStatus.NOT_FOUND) +public class IdentityProviderNotFoundException extends NotFoundException { + + private static final String IDENTITY_PROVIDER_NOT_FOUND_MESSAGE = "Identity provider with alias %s not found in keycloak."; + + public IdentityProviderNotFoundException(String identityProviderAlias) { + super(String.format(IDENTITY_PROVIDER_NOT_FOUND_MESSAGE, identityProviderAlias)); + } +} diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/model/ExtensibleModel.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/ExtensibleModel.java new file mode 100644 index 0000000..f81a919 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/ExtensibleModel.java @@ -0,0 +1,10 @@ +package com.knecon.fforesight.tenantusermanagement.model; + +import java.util.Map; + +public interface ExtensibleModel { + + void setNotMappedFields(Map notMappedFields); + Map getNotMappedFields(); + +} diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderConfigModel.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderConfigModel.java new file mode 100644 index 0000000..5a54b49 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderConfigModel.java @@ -0,0 +1,128 @@ +package com.knecon.fforesight.tenantusermanagement.model; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Object containing information about an identity provider configuration.") +public class IdentityProviderConfigModel implements ExtensibleModel { + + @Builder.Default + @Schema(description = "Flag indicating whether creation is allowed (default: true)") + private Boolean allowCreate = true; + + @Builder.Default + @Schema(description = "Order for GUI display (default: 0)") + private Integer guiOrder = 0; + + @Schema(description = "Entity ID") + private String entityId; + + @Schema(description = "Identity Provider Entity ID") + private String idpEntityId; + + @Schema(description = "Single Sign-On Service URL") + private String singleSignOnServiceUrl; + + @Builder.Default + @Schema(description = "Single Logout Service URL (default: empty string)") + private String singleLogoutServiceUrl = ""; + + @Builder.Default + @Schema(description = "Attribute Consuming Service Name (default: empty string)") + private String attributeConsumingServiceName = ""; + + @Builder.Default + @Schema(description = "Flag indicating backchannel support (default: false)") + private Boolean backchannelSupported = false; + + @Builder.Default + @Schema(description = "NameID Policy Format (default: PERSISTENT)") + private IdentityProviderNameIDPolicyFormat nameIDPolicyFormat = IdentityProviderNameIDPolicyFormat.PERSISTENT; + + @Builder.Default + @Schema(description = "Principal Type (default: SUBJECT_NAME_ID)") + private IdentityProviderPrincipalType principalType = IdentityProviderPrincipalType.SUBJECT; + + @Builder.Default + @Schema(description = "Flag indicating post-binding response support (default: false)") + private Boolean postBindingResponse = false; + + @Builder.Default + @Schema(description = "Flag indicating post-binding authentication request support (default: false)") + private Boolean postBindingAuthnRequest = false; + + @Builder.Default + @Schema(description = "Flag indicating post-binding logout support (default: false)") + private Boolean postBindingLogout = false; + + @Builder.Default + @Schema(description = "Flag indicating whether Authn requests should be signed (default: false)") + private Boolean wantAuthnRequestsSigned = false; + + @Builder.Default + @Schema(description = "Flag indicating whether assertions should be signed (default: false)") + private Boolean wantAssertionsSigned = false; + + @Builder.Default + @Schema(description = "Flag indicating whether assertions should be encrypted (default: false)") + private Boolean wantAssertionsEncrypted = false; + + @Builder.Default + @Schema(description = "Flag indicating whether force authentication is required (default: false)") + private Boolean forceAuthn = false; + + @Builder.Default + @Schema(description = "Flag indicating whether signature validation is required (default: false)") + private Boolean validateSignature = false; + + @Builder.Default + @Schema(description = "Flag indicating whether to sign SP metadata (default: false)") + private Boolean signSpMetadata = false; + + @Builder.Default + @Schema(description = "Flag indicating whether login hInteger is supported (default: false)") + private Boolean loginHInteger = false; + + @Builder.Default + @Schema(description = "Allowed clock skew in seconds (default: 0)") + private Integer allowedClockSkew = 0; + + @Builder.Default + @Schema(description = "Attribute Consuming Service Index (default: 0)") + private Integer attributeConsumingServiceIndex = 0; + + @Builder.Default + @Schema(description = "Flag indicating whether login is only possible if requested explicitly (default: false)") + private Boolean hideOnLoginPage = false; + + @Builder.Default + @Schema(description = "Sync mode of the mapper (default: IMPORT)") + @JsonInclude(JsonInclude.Include.NON_NULL) + private IdentityProviderSyncMode syncMode = IdentityProviderSyncMode.IMPORT; + + @JsonInclude(JsonInclude.Include.NON_NULL) + @Schema(description = "Sync mode of the mapper (only used when wantAuthnRequestsSigned is true)") + private IdentityProviderSignatureAlgorithm signatureAlgorithm; + + @Schema(description = "SAML signature key name (only used when wantAuthnRequestsSigned is true)") + @JsonInclude(JsonInclude.Include.NON_NULL) + private IdentityProviderSAMLSignatureKeyName xmlSigKeyInfoKeyNameTransformer; + + @Builder.Default + @Schema(description = "All not mapped configurations") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Map notMappedFields = new HashMap<>(); + +} diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderConfigRequest.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderConfigRequest.java new file mode 100644 index 0000000..017d302 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderConfigRequest.java @@ -0,0 +1,62 @@ +package com.knecon.fforesight.tenantusermanagement.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Object containing information about an identity provider configuration.") +public class IdentityProviderConfigRequest { + + @Builder.Default + @Schema(description = "Flag indicating whether creation is allowed (default: true)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private Boolean allowCreate = true; + + @Builder.Default + @Schema(description = "Order for GUI display (default: 0)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private Integer guiOrder = 0; + + @Schema(description = "Entity ID") + private String entityId; + + @Schema(description = "Identity Provider Entity ID") + private String idpEntityId; + + @Schema(description = "Single Sign-On Service URL") + private String singleSignOnServiceUrl; + + @Builder.Default + @Schema(description = "NameID Policy Format (default: PERSISTENT)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private IdentityProviderNameIDPolicyFormat nameIDPolicyFormat = IdentityProviderNameIDPolicyFormat.PERSISTENT; + + @Builder.Default + @Schema(description = "Principal Type (default: SUBJECT)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private IdentityProviderPrincipalType principalType = IdentityProviderPrincipalType.SUBJECT; + + @Builder.Default + @Schema(description = "Flag indicating post-binding response support (default: false)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private Boolean postBindingResponse = false; + + @Builder.Default + @Schema(description = "Flag indicating post-binding authentication request support (default: false)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private Boolean postBindingAuthnRequest = false; + + @Builder.Default + @Schema(description = "Flag indicating whether Authn requests should be signed (default: false)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private Boolean wantAuthnRequestsSigned = false; + + @Builder.Default + @Schema(description = "Sync mode of the mapper (only used when wantAuthnRequestsSigned is true)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private IdentityProviderSignatureAlgorithm signatureAlgorithm = IdentityProviderSignatureAlgorithm.RSA_SHA256; + + @Builder.Default + @Schema(description = "SAML signature key name (only used when wantAuthnRequestsSigned is true)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private IdentityProviderSAMLSignatureKeyName xmlSigKeyInfoKeyNameTransformer = IdentityProviderSAMLSignatureKeyName.KEY_ID; + + +} diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderList.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderList.java new file mode 100644 index 0000000..2730d71 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderList.java @@ -0,0 +1,19 @@ +package com.knecon.fforesight.tenantusermanagement.model; + +import java.util.ArrayList; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IdentityProviderList { + + @Builder.Default + private List identityProviders = new ArrayList<>(); +} diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderModel.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderModel.java new file mode 100644 index 0000000..bd35dd6 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderModel.java @@ -0,0 +1,69 @@ +package com.knecon.fforesight.tenantusermanagement.model; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.Builder; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Object containing information about an identity provider.") +public class IdentityProviderModel implements ExtensibleModel { + + @Schema(description = "Alias of the identity provider") + private String alias; + + @Builder.Default + @Schema(description = "Configuration of the identity provider") + private IdentityProviderConfigModel config = new IdentityProviderConfigModel(); + + @Builder.Default + @Schema(description = "Display name of the identity provider (optional)") + private String displayName = ""; + + @Builder.Default + @Schema(description = "Provider ID of the identity provider") + private String providerId = "saml"; + + @Builder.Default + @Schema(description = "Whether tokens are to be stored (optional)") + private Boolean storeToken = false; + + @Builder.Default + @Schema(description = "Whether new users can read any stored tokens (optional)") + private Boolean addReadTokenRoleOnCreate = false; + + @Builder.Default + @Schema(description = "Whether emails are to be trusted (optional)") + private Boolean trustEmail = false; + + @Builder.Default + @Schema(description = "Whether users can only link and not log in through this provider (optional)") + private Boolean linkOnly = false; + + @Builder.Default + @Schema(description = "Alias of the authentication flow, which is triggered after first login with this identity provider (optional)") + private String firstBrokerLoginFlowAlias = "first broker login"; + + @Builder.Default + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Map notMappedFields = new HashMap<>(); + + + public void setSpecificDefaults() { + + this.setStoreToken(true); + this.setTrustEmail(true); + this.getConfig().setSyncMode(IdentityProviderSyncMode.FORCE); + } + +} + diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderNameIDPolicyFormat.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderNameIDPolicyFormat.java new file mode 100644 index 0000000..49eab97 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderNameIDPolicyFormat.java @@ -0,0 +1,43 @@ +package com.knecon.fforesight.tenantusermanagement.model; + +import com.fasterxml.jackson.annotation.JsonValue; + +import jakarta.ws.rs.BadRequestException; +import lombok.Getter; + +@Getter +public enum IdentityProviderNameIDPolicyFormat { + PERSISTENT("urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"), + TRANSIENT("urn:oasis:names:tc:SAML:2.0:nameid-format:transient"), + EMAIL("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"), + KERBEROS("urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos"), + X_509_SUBJECT_NAME("urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName"), + WINDOWS_DOMAIN_QUALIFIED_NAME("urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName"), + UNSPECIFIED("urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"); + + private final String representation; + + + IdentityProviderNameIDPolicyFormat(String representation) { + + this.representation = representation; + } + + + @Override + @JsonValue + public String toString() { + + return representation; + } + + public static IdentityProviderNameIDPolicyFormat fromRepresentation(String value) { + + for (IdentityProviderNameIDPolicyFormat v : values()) { + if (v.getRepresentation().equalsIgnoreCase(value)) { + return v; + } + } + throw new BadRequestException("Unsupported identity provider name ID policy format"); + } +} \ No newline at end of file diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderPrincipalType.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderPrincipalType.java new file mode 100644 index 0000000..cf0bec0 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderPrincipalType.java @@ -0,0 +1,24 @@ +package com.knecon.fforesight.tenantusermanagement.model; + + +import jakarta.ws.rs.BadRequestException; + +public enum IdentityProviderPrincipalType { + SUBJECT, + ATTRIBUTE, + FRIENDLY_ATTRIBUTE; + + + public static IdentityProviderPrincipalType fromStringValue(String value) { + + for (IdentityProviderPrincipalType v : values()) { + if (v.toString().equalsIgnoreCase(value)) { + return v; + } + if (value.equalsIgnoreCase("Subject NameID")) { + return SUBJECT; + } + } + throw new BadRequestException("Unsupported identity provider principal type"); + } +} \ No newline at end of file diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderRequest.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderRequest.java new file mode 100644 index 0000000..10dbafe --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderRequest.java @@ -0,0 +1,31 @@ +package com.knecon.fforesight.tenantusermanagement.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Create or update request for an identity provider.") +public class IdentityProviderRequest { + + @Schema(description = "Alias of the identity provider") + private String alias; + + @Builder.Default + @Schema(description = "Configuration of the identity provider") + private IdentityProviderConfigRequest config = new IdentityProviderConfigRequest(); + + @Builder.Default + @Schema(description = "Display name of the identity provider (optional)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String displayName = ""; + + @Builder.Default + @Schema(description = "Provider ID of the identity provider", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String providerId = "saml"; + +} diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderSAMLSignatureKeyName.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderSAMLSignatureKeyName.java new file mode 100644 index 0000000..3be9fe1 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderSAMLSignatureKeyName.java @@ -0,0 +1,7 @@ +package com.knecon.fforesight.tenantusermanagement.model; + +public enum IdentityProviderSAMLSignatureKeyName { + NONE, + KEY_ID, + CERT_SUBJECT +} diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderSignatureAlgorithm.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderSignatureAlgorithm.java new file mode 100644 index 0000000..290ee0a --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderSignatureAlgorithm.java @@ -0,0 +1,10 @@ +package com.knecon.fforesight.tenantusermanagement.model; + +public enum IdentityProviderSignatureAlgorithm { + RSA_SHA1, + RSA_SHA256, + RSA_SHA256_MGF1, + RSA_SHA512, + RSA_SHA512_MGF1, + DSA_SHA1 +} diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderSyncMode.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderSyncMode.java new file mode 100644 index 0000000..6a64010 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderSyncMode.java @@ -0,0 +1,8 @@ +package com.knecon.fforesight.tenantusermanagement.model; + +public enum IdentityProviderSyncMode { + INHERIT, + IMPORT, + LEGACY, + FORCE +} diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderWithDescriptorRequest.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderWithDescriptorRequest.java new file mode 100644 index 0000000..b8fe13d --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/model/IdentityProviderWithDescriptorRequest.java @@ -0,0 +1,33 @@ +package com.knecon.fforesight.tenantusermanagement.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Create or update request for an identity provider.") +public class IdentityProviderWithDescriptorRequest { + + @Schema(description = "Alias of the identity provider", requiredMode = Schema.RequiredMode.REQUIRED) + private String alias; + + @Builder.Default + @Schema(description = "Display name of the identity provider (optional)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String displayName = ""; + + @Builder.Default + @Schema(description = "Provider ID of the identity provider", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + private String providerId = "saml"; + + @Schema(description = "External IDP metadata from a URL", requiredMode = Schema.RequiredMode.REQUIRED) + private String samlEntityDescriptorURL; + + @Schema(description = "Display name of the identity provider (optional)", requiredMode = Schema.RequiredMode.REQUIRED) + private String entityId; + +} diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/permissions/UserManagementPermissions.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/permissions/UserManagementPermissions.java index 5737d0d..25d1bbb 100644 --- a/src/main/java/com/knecon/fforesight/tenantusermanagement/permissions/UserManagementPermissions.java +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/permissions/UserManagementPermissions.java @@ -27,4 +27,8 @@ public class UserManagementPermissions { public static final String READ_SMTP_CONFIGURATION = "fforesight-read-smtp-configuration"; public static final String WRITE_SMTP_CONFIGURATION = "fforesight-write-smtp-configuration"; + // Identity provider + public static final String READ_IDENTITY_PROVIDER_CONFIGURATION = "fforesight-read-identity-provider-config"; + public static final String WRITE_IDENTITY_PROVIDER_CONFIGURATION = "fforesight-write-identity-provider-config"; + } diff --git a/src/main/java/com/knecon/fforesight/tenantusermanagement/utils/IdentityProviderMappingService.java b/src/main/java/com/knecon/fforesight/tenantusermanagement/utils/IdentityProviderMappingService.java new file mode 100644 index 0000000..c6e35b3 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/tenantusermanagement/utils/IdentityProviderMappingService.java @@ -0,0 +1,179 @@ +package com.knecon.fforesight.tenantusermanagement.utils; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; + +import org.keycloak.representations.idm.IdentityProviderRepresentation; + +import com.knecon.fforesight.databasetenantcommons.providers.utils.MagicConverter; +import com.knecon.fforesight.tenantusermanagement.model.ExtensibleModel; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderConfigModel; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderModel; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderNameIDPolicyFormat; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderPrincipalType; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderRequest; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderWithDescriptorRequest; + +import lombok.SneakyThrows; + +public class IdentityProviderMappingService { + + public static IdentityProviderRepresentation toRepresentationFromRequest(IdentityProviderRequest identityProviderRequest) { + + return toRepresentationFromModel(toModelFromRequest(identityProviderRequest)); + + } + + + public static IdentityProviderModel toModelFromRequest(IdentityProviderRequest identityProviderRequest) { + + if (!identityProviderRequest.getConfig().getWantAuthnRequestsSigned()) { + identityProviderRequest.getConfig().setXmlSigKeyInfoKeyNameTransformer(null); + identityProviderRequest.getConfig().setSignatureAlgorithm(null); + } + return MagicConverter.convert(identityProviderRequest, IdentityProviderModel.class, getModelFromRequestDeltaMapper()); + + } + + + private static BiConsumer getModelFromRequestDeltaMapper() { + + return ((identityProviderRequest, identityProviderModel) -> { + var identityProviderConfigModel = MagicConverter.convert(identityProviderRequest.getConfig(), IdentityProviderConfigModel.class); + identityProviderModel.setConfig(identityProviderConfigModel); + }); + } + + + public static IdentityProviderRepresentation toRepresentationFromModel(IdentityProviderModel identityProviderModel) { + + return MagicConverter.convert(identityProviderModel, IdentityProviderRepresentation.class, getModelToRepresentationDeltaMapper()); + } + + + private static BiConsumer getModelToRepresentationDeltaMapper() { + + return ((identityProviderModel, identityProviderRepresentation) -> identityProviderRepresentation.setConfig(convertObjectToMap(identityProviderModel.getConfig()))); + } + + + public static IdentityProviderModel toModelFromDescriptorRequestAndConfig(IdentityProviderWithDescriptorRequest identityProvider, Map configurationsMap) { + + IdentityProviderModel identityProviderModel = MagicConverter.convert(identityProvider, IdentityProviderModel.class); + identityProviderModel.setConfig(convertMapToObject(configurationsMap, IdentityProviderConfigModel.class)); + return identityProviderModel; + } + + + @SneakyThrows + public static Map convertObjectToMap(Object obj) { + + Map resultMap = new HashMap<>(); + Class clazz = obj.getClass(); + + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + PropertyDescriptor pd = new PropertyDescriptor(field.getName(), clazz); + Method getter = pd.getReadMethod(); + Object fieldValue = getter.invoke(obj); + if (fieldValue != null) { + resultMap.put(field.getName(), fieldValue.toString()); + } + } + + return resultMap; + } + + + public static IdentityProviderModel toModelFromRepresentation(IdentityProviderRepresentation identityProviderRepresentation) { + + return MagicConverter.convert(identityProviderRepresentation, IdentityProviderModel.class, getRepresentationToModelDeltaMapper()); + } + + + private static BiConsumer getRepresentationToModelDeltaMapper() { + + return ((identityProviderRepresentation, identityProviderModel) -> identityProviderModel.setConfig(convertMapToObject(identityProviderRepresentation.getConfig(), + IdentityProviderConfigModel.class))); + } + + + @SneakyThrows + public static T convertMapToObject(Map map, Class clazz) { + + T obj = clazz.getDeclaredConstructor().newInstance(); + Field[] fields = clazz.getDeclaredFields(); + sanitizeMapKeys(map); + for (Field field : fields) { + String fieldName = field.getName(); + if (map.containsKey(fieldName)) { + String fieldValue = map.get(fieldName); + setFieldValue(obj, field, fieldValue); + map.remove(fieldName); + } + } + + obj.setNotMappedFields(map); + + return obj; + } + + + private static void sanitizeMapKeys(Map inputMap) { + + var keys = new ArrayList<>(inputMap.keySet()); + for (String key : keys) { + String sanitizedKey = sanitize(key); + if (!sanitizedKey.equals(key)) { + String value = inputMap.remove(key); + inputMap.put(sanitizedKey, value); + } + } + } + + + private static String sanitize(String input) { + + StringBuilder result = new StringBuilder(); + String[] segments = input.split("\\."); + for (String segment : segments) { + if (!result.isEmpty()) { + result.append(Character.toUpperCase(segment.charAt(0))); + result.append(segment.substring(1)); + } else { + result.append(segment); + } + } + return result.toString(); + } + + + @SneakyThrows + private static void setFieldValue(T obj, Field field, String value) { + + Class fieldType = field.getType(); + PropertyDescriptor pd = new PropertyDescriptor(field.getName(), obj.getClass()); + Method setter = pd.getWriteMethod(); + + if (fieldType == String.class) { + setter.invoke(obj, value); + } else if (fieldType == int.class || fieldType == Integer.class) { + setter.invoke(obj, Integer.parseInt(value)); + } else if (fieldType == boolean.class || fieldType == Boolean.class) { + setter.invoke(obj, Boolean.parseBoolean(value)); + } else if (fieldType == IdentityProviderNameIDPolicyFormat.class) { + setter.invoke(obj, IdentityProviderNameIDPolicyFormat.fromRepresentation(value)); + } else if (fieldType == IdentityProviderPrincipalType.class) { + setter.invoke(obj, IdentityProviderPrincipalType.fromStringValue(value)); + } else if (fieldType.isEnum()) { + Enum enumValue = Enum.valueOf(fieldType.asSubclass(Enum.class), value); + setter.invoke(obj, enumValue); + } + } + +} diff --git a/src/main/resources/application-clarifynd.yaml b/src/main/resources/application-clarifynd.yaml index 0da0729..a4fb7ba 100644 --- a/src/main/resources/application-clarifynd.yaml +++ b/src/main/resources/application-clarifynd.yaml @@ -45,6 +45,8 @@ fforesight: - "fforesight-write-users" - "fforesight-read-smtp-configuration" - "fforesight-write-smtp-configuration" + - "fforesight-read-identity-provider-config" + - "fforesight-write-identity-provider-config" - "red-unarchive-dossier" - name: FF_ADMIN set-by-default: true @@ -63,6 +65,8 @@ fforesight: - 'fforesight-deployment-info' - 'fforesight-read-smtp-configuration' - 'fforesight-write-smtp-configuration' + - "fforesight-read-identity-provider-config" + - "fforesight-write-identity-provider-config" - 'fforesight-search' - 'fforesight-search-audit-log' - 'fforesight-view-document' diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml index 211f9e5..40d75a2 100644 --- a/src/main/resources/application-dev.yaml +++ b/src/main/resources/application-dev.yaml @@ -28,6 +28,9 @@ fforesight: - 'fforesight-deployment-info' - 'fforesight-read-smtp-configuration' - 'fforesight-write-smtp-configuration' + - 'fforesight-read-identity-provider-config' + - 'fforesight-write-identity-provider-config' + application-name: "redaction" springdoc: auth-server-url: http://localhost:8080 diff --git a/src/main/resources/application-documine.yaml b/src/main/resources/application-documine.yaml index 0db460c..f9da110 100644 --- a/src/main/resources/application-documine.yaml +++ b/src/main/resources/application-documine.yaml @@ -22,7 +22,7 @@ fforesight: - name: KNECON_ADMIN set-by-default: false rank: 1000 - permissions: [ "red-read-license", "red-update-license","fforesight-get-tenants", "fforesight-create-tenant", "fforesight-update-tenant", "fforesight-delete-tenant","fforesight-read-users", "fforesight-read-all-users", "fforesight-write-users","fforesight-read-smtp-configuration", "fforesight-write-smtp-configuration","red-unarchive-dossier" ] + permissions: [ "red-read-license", "red-update-license","fforesight-get-tenants", "fforesight-create-tenant", "fforesight-update-tenant", "fforesight-delete-tenant","fforesight-read-users", "fforesight-read-all-users", "fforesight-write-users","fforesight-read-smtp-configuration", "fforesight-write-smtp-configuration", "fforesight-read-identity-provider-config","fforesight-write-identity-provider-config", "red-unarchive-dossier" ] - name: RED_USER set-by-default: true rank: 100 @@ -41,8 +41,8 @@ fforesight: permissions: [ "red-add-dictionary-entry", "red-add-update-dictionary-type", "red-write-dossier-status", "red-read-dossier-status", "red-delete-dictionary-entry", "red-delete-dictionary-type", "red-delete-report-template", "red-download-report-template", "red-get-report-templates", "fforesight-manage-user-preferences", "red-read-colors", "red-read-dictionary-types", "red-read-digital-signature", "red-read-dossier-attributes", "red-read-dossier-attributes-config", "red-read-dossier-templates", "red-read-file-attributes-config", - "red-read-legal-basis", "red-read-license-report", "red-read-notification", "red-read-rules", "fforesight-read-smtp-configuration", "red-read-versions", "red-reindex", "red-search-audit-log", "red-update-notification", "red-upload-report-template", "red-write-colors", "red-write-digital-signature", "red-write-dossier-attributes-config", - "red-write-dossier-templates", "red-write-file-attributes-config", "fforesight-write-general-configuration", "red-write-legal-basis", "red-write-rules", "fforesight-write-smtp-configuration", "red-write-app-configuration", "red-manage-acl-permissions", "fforesight-create-tenant", "fforesight-get-tenants", "fforesight-update-tenant", "fforesight-deployment-info" ] + "red-read-legal-basis", "red-read-license-report", "red-read-notification", "red-read-rules", "fforesight-read-smtp-configuration", "fforesight-read-identity-provider-config", "red-read-versions", "red-reindex", "red-search-audit-log", "red-update-notification", "red-upload-report-template", "red-write-colors", "red-write-digital-signature", "red-write-dossier-attributes-config", + "red-write-dossier-templates", "red-write-file-attributes-config", "fforesight-write-general-configuration", "red-write-legal-basis", "red-write-rules", "fforesight-write-smtp-configuration", "fforesight-write-identity-provider-config", "red-write-app-configuration", "red-manage-acl-permissions", "fforesight-create-tenant", "fforesight-get-tenants", "fforesight-update-tenant", "fforesight-deployment-info" ] - name: RED_MANAGER set-by-default: false rank: 200 diff --git a/src/main/resources/application-redaction.yaml b/src/main/resources/application-redaction.yaml index 980b61e..5bcca12 100644 --- a/src/main/resources/application-redaction.yaml +++ b/src/main/resources/application-redaction.yaml @@ -6,7 +6,7 @@ fforesight: tenant-access-token-life-span: 300 realm: master default-theme: 'redaction' - valid-redirect-uris: [ '/api/*','/redaction-gateway-v1/*','/tenant-user-management/*','http://localhost:4200/*','/ui/*' ,'/auth/*'] + valid-redirect-uris: [ '/api/*','/redaction-gateway-v1/*','/tenant-user-management/*','http://localhost:4200/*','/ui/*' ,'/auth/*' ] kc-role-mapping: unmappedPermissions: [ "red-unarchive-dossier", "red-update-license", "red-get-rss","fforesight-create-tenant", "fforesight-update-tenant", "red-experimental" ] compositeRoles: @@ -37,9 +37,9 @@ fforesight: permissions: [ "red-add-dictionary-entry", "red-add-update-dictionary-type", "red-write-dossier-status", "red-read-dossier-status", "red-delete-dictionary-entry", "red-delete-dictionary-type", "red-delete-report-template", "red-download-report-template", "red-get-report-templates", "fforesight-manage-user-preferences", "red-read-colors", "red-read-dictionary-types", "red-read-digital-signature", "red-read-dossier-attributes", "red-read-dossier-attributes-config", "red-read-dossier-templates", "red-read-file-attributes-config", - "red-read-legal-basis", "red-read-license-report", "red-read-notification", "red-read-rules", "fforesight-read-smtp-configuration", "red-read-versions", "red-read-watermark", + "red-read-legal-basis", "red-read-license-report", "red-read-notification", "red-read-rules", "fforesight-read-smtp-configuration", "fforesight-read-identity-provider-config", "red-read-versions", "red-read-watermark", "red-reindex", "red-search-audit-log", "red-update-notification", "red-upload-report-template", "red-write-colors", "red-write-digital-signature", "red-write-dossier-attributes-config", - "red-write-dossier-templates", "red-write-file-attributes-config", "fforesight-write-general-configuration", "red-write-legal-basis", "red-write-rules", "fforesight-write-smtp-configuration", + "red-write-dossier-templates", "red-write-file-attributes-config", "fforesight-write-general-configuration", "red-write-legal-basis", "red-write-rules", "fforesight-write-smtp-configuration", "fforesight-write-identity-provider-config", "red-write-watermark", "red-write-app-configuration", "red-manage-acl-permissions", "fforesight-create-tenant", "fforesight-get-tenants", "fforesight-update-tenant", "fforesight-deployment-info" ] - name: RED_MANAGER set-by-default: false @@ -48,7 +48,7 @@ fforesight: - name: KNECON_ADMIN set-by-default: false rank: 1000 - permissions: ["red-read-license", "red-update-license","fforesight-get-tenants", "fforesight-create-tenant", "fforesight-update-tenant", "fforesight-delete-tenant","fforesight-read-users", "fforesight-read-all-users", "fforesight-write-users","fforesight-read-smtp-configuration", "fforesight-write-smtp-configuration","red-unarchive-dossier"] + permissions: [ "red-read-license", "red-update-license","fforesight-get-tenants", "fforesight-create-tenant", "fforesight-update-tenant", "fforesight-delete-tenant","fforesight-read-users", "fforesight-read-all-users", "fforesight-write-users","fforesight-read-smtp-configuration", "fforesight-write-smtp-configuration","red-unarchive-dossier" ] - name: RED_USER_ADMIN set-by-default: false rank: 400 diff --git a/src/test/java/com/knecon/fforesight/tenantusermanagement/feigntestclients/external/IdentityProviderConfigurationClient.java b/src/test/java/com/knecon/fforesight/tenantusermanagement/feigntestclients/external/IdentityProviderConfigurationClient.java new file mode 100644 index 0000000..223ca95 --- /dev/null +++ b/src/test/java/com/knecon/fforesight/tenantusermanagement/feigntestclients/external/IdentityProviderConfigurationClient.java @@ -0,0 +1,10 @@ +package com.knecon.fforesight.tenantusermanagement.feigntestclients.external; + +import org.springframework.cloud.openfeign.FeignClient; + +import com.knecon.fforesight.tenantusermanagement.api.external.IdentityProviderConfigurationResource; + +@FeignClient(name = "IdentityProviderConfigurationClient", url = "http://localhost:${server.port}", path = "${fforesight.tenant-user-management.base-path:}") +public interface IdentityProviderConfigurationClient extends IdentityProviderConfigurationResource { + +} diff --git a/src/test/java/com/knecon/fforesight/tenantusermanagement/tests/IdentityProviderConfigurationTest.java b/src/test/java/com/knecon/fforesight/tenantusermanagement/tests/IdentityProviderConfigurationTest.java new file mode 100644 index 0000000..becec5b --- /dev/null +++ b/src/test/java/com/knecon/fforesight/tenantusermanagement/tests/IdentityProviderConfigurationTest.java @@ -0,0 +1,144 @@ +package com.knecon.fforesight.tenantusermanagement.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import com.knecon.fforesight.tenantcommons.TenantContext; +import com.knecon.fforesight.tenantusermanagement.AbstractTenantUserManagementIntegrationTest; +import com.knecon.fforesight.tenantusermanagement.feigntestclients.external.IdentityProviderConfigurationClient; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderConfigRequest; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderNameIDPolicyFormat; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderPrincipalType; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderRequest; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderSAMLSignatureKeyName; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderSignatureAlgorithm; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderSyncMode; +import com.knecon.fforesight.tenantusermanagement.model.IdentityProviderWithDescriptorRequest; + +import feign.FeignException; +import lombok.AccessLevel; +import lombok.experimental.FieldDefaults; + +@FieldDefaults(level = AccessLevel.PRIVATE) +public class IdentityProviderConfigurationTest extends AbstractTenantUserManagementIntegrationTest { + + @Autowired + IdentityProviderConfigurationClient identityProviderConfigurationClient; + + final String testAlias = "testAlias"; + final String entityId = "https://sso-test.iqser.cloud"; + 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"; + + + @Test + public void testIdentityProviderConfiguration() { + + TenantContext.setTenantId(AbstractTenantUserManagementIntegrationTest.TEST_TENANT_ID); + + var createdResponse = identityProviderConfigurationClient.createIdentityProvider(provideIdentityProviderRequestCreate()); + assertEquals(201, createdResponse.getStatusCode().value()); + var identityProviderModel = createdResponse.getBody(); + assert identityProviderModel != null; + assertEquals(identityProviderModel.getAlias(), testAlias); + assertEquals(identityProviderModel.getConfig().getEntityId(), entityId); + assertEquals(identityProviderModel.getConfig().getIdpEntityId(), idpEntityId); + assertEquals(identityProviderModel.getConfig().getSingleSignOnServiceUrl(), ssoServiceUrl); + assertTrue(identityProviderModel.getTrustEmail()); + assertTrue(identityProviderModel.getStoreToken()); + assertEquals(identityProviderModel.getConfig().getSyncMode(), IdentityProviderSyncMode.FORCE); + + var updatedResponse = identityProviderConfigurationClient.updateIdentityProvider(testAlias, provideIdentityProviderRequestUpdate()); + assertEquals(200, updatedResponse.getStatusCode().value()); + identityProviderModel = updatedResponse.getBody(); + assert identityProviderModel != null; + assertEquals(identityProviderModel.getConfig().getNameIDPolicyFormat(), IdentityProviderNameIDPolicyFormat.TRANSIENT); + assertEquals(identityProviderModel.getConfig().getPrincipalType(), IdentityProviderPrincipalType.ATTRIBUTE); + assertFalse(identityProviderModel.getConfig().getAllowCreate()); + assertTrue(identityProviderModel.getConfig().getWantAuthnRequestsSigned()); + assertEquals(identityProviderModel.getConfig().getXmlSigKeyInfoKeyNameTransformer(), IdentityProviderSAMLSignatureKeyName.CERT_SUBJECT); + assertEquals(identityProviderModel.getConfig().getSignatureAlgorithm(), IdentityProviderSignatureAlgorithm.RSA_SHA512); + var identityProviderList = identityProviderConfigurationClient.getIdentityProviders(); + assertEquals(identityProviderList.getIdentityProviders().size(), 1); + + var identityProvider = identityProviderConfigurationClient.getIdentityProvider(testAlias); + assertEquals(identityProviderModel.getAlias(), testAlias); + assertEquals(identityProviderModel.getConfig().getEntityId(), entityId); + assertEquals(identityProviderModel.getConfig().getIdpEntityId(), idpEntityId); + assertEquals(identityProviderModel.getConfig().getSingleSignOnServiceUrl(), ssoServiceUrl); + assertTrue(identityProviderModel.getTrustEmail()); + assertTrue(identityProviderModel.getStoreToken()); + assertEquals(identityProviderModel.getConfig().getSyncMode(), IdentityProviderSyncMode.FORCE); + assertEquals(identityProviderModel.getConfig().getNameIDPolicyFormat(), IdentityProviderNameIDPolicyFormat.TRANSIENT); + assertEquals(identityProviderModel.getConfig().getPrincipalType(), IdentityProviderPrincipalType.ATTRIBUTE); + assertFalse(identityProviderModel.getConfig().getAllowCreate()); + assertTrue(identityProviderModel.getConfig().getWantAuthnRequestsSigned()); + assertEquals(identityProviderModel.getConfig().getXmlSigKeyInfoKeyNameTransformer(), IdentityProviderSAMLSignatureKeyName.CERT_SUBJECT); + assertEquals(identityProviderModel.getConfig().getSignatureAlgorithm(), IdentityProviderSignatureAlgorithm.RSA_SHA512); + + var createdDescriptorResponse = identityProviderConfigurationClient.createIdentityProviderFromDescriptor(provideIdentityProviderWithDescriptorRequest()); + assertEquals(createdDescriptorResponse.getStatusCode().value(), 201); + + identityProviderList = identityProviderConfigurationClient.getIdentityProviders(); + assertEquals(identityProviderList.getIdentityProviders().size(), 2); + + identityProviderConfigurationClient.deleteIdentityProvider(testAliasDescriptor); + identityProviderList = identityProviderConfigurationClient.getIdentityProviders(); + assertEquals(identityProviderList.getIdentityProviders().size(), 1); + + var e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.createIdentityProvider(provideIdentityProviderRequestCreate())); + assertEquals(409, e.status()); + + e = assertThrows(FeignException.class, () -> identityProviderConfigurationClient.getIdentityProvider(testAliasDescriptor)); + assertEquals(404, e.status()); + } + + + private IdentityProviderRequest provideIdentityProviderRequestCreate() { + + return IdentityProviderRequest.builder() + .alias(testAlias) + .config(IdentityProviderConfigRequest.builder() + .entityId(entityId) + .idpEntityId(idpEntityId) + .singleSignOnServiceUrl(ssoServiceUrl) + .nameIDPolicyFormat(IdentityProviderNameIDPolicyFormat.PERSISTENT) + .principalType(IdentityProviderPrincipalType.SUBJECT) + .build()) + .build(); + } + + private IdentityProviderRequest provideIdentityProviderRequestUpdate() { + + return IdentityProviderRequest.builder() + .config(IdentityProviderConfigRequest.builder() + .entityId(entityId) + .idpEntityId(idpEntityId) + .singleSignOnServiceUrl(ssoServiceUrl) + .nameIDPolicyFormat(IdentityProviderNameIDPolicyFormat.TRANSIENT) + .principalType(IdentityProviderPrincipalType.ATTRIBUTE) + .allowCreate(false) + .wantAuthnRequestsSigned(true) + .xmlSigKeyInfoKeyNameTransformer(IdentityProviderSAMLSignatureKeyName.CERT_SUBJECT) + .signatureAlgorithm(IdentityProviderSignatureAlgorithm.RSA_SHA512) + .build()) + .build(); + } + + + private IdentityProviderWithDescriptorRequest provideIdentityProviderWithDescriptorRequest() { + + return IdentityProviderWithDescriptorRequest.builder() + .alias(testAliasDescriptor) + .entityId(entityId) + .samlEntityDescriptorURL("https://login.microsoftonline.com/b44be368-e4f2-4ade-a089-cd2825458048/federationmetadata/2007-06/federationmetadata.xml") + .build(); + } + +} diff --git a/src/test/resources/application.yaml b/src/test/resources/application.yaml index c38e340..b13dffd 100644 --- a/src/test/resources/application.yaml +++ b/src/test/resources/application.yaml @@ -123,6 +123,8 @@ fforesight: - 'fforesight-deployment-info' - 'fforesight-read-smtp-configuration' - 'fforesight-write-smtp-configuration' + - 'fforesight-read-identity-provider-config' + - 'fforesight-write-identity-provider-config' - name: LESS_SUPER_USER set-by-default: true rank: 10 @@ -140,6 +142,8 @@ fforesight: - 'fforesight-deployment-info' - 'fforesight-read-smtp-configuration' - 'fforesight-write-smtp-configuration' + - 'fforesight-read-identity-provider-config' + - 'fforesight-write-identity-provider-config' access-token-life-span: 86400 application-name: tenant-user-management application-client-id: tenant-user-management