Merge branch 'RED-8477' into 'main'

RED-8477: SSO settings endpoint for SAML

See merge request fforesight/tenant-user-management-service!86
This commit is contained in:
Maverick Studer 2024-02-13 12:42:21 +01:00
commit 7bec8f98e6
29 changed files with 1177 additions and 16 deletions

View File

@ -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")

View File

@ -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<IdentityProviderModel> 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<IdentityProviderModel> 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<IdentityProviderModel> 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);
}

View File

@ -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<TenantResponse> 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<SimpleTenantResponse> 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);

View File

@ -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<ErrorMessage> handleNotFound(NotFoundException e) {
return new ResponseEntity<>(new ErrorMessage(e.getMessage()), HttpStatus.NOT_FOUND);
}
@ExceptionHandler(ForbiddenException.class)
public ResponseEntity<ErrorMessage> 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<ErrorMessage> handleBadRequestException(BadRequestException e) {

View File

@ -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<IdentityProviderModel> createIdentityProvider(IdentityProviderRequest identityProvider) {
IdentityProviderModel identityProviderModel = IdentityProviderMappingService.toModelFromRequest(identityProvider);
return callKeyCloakIdentityProvidersCreateForModel(identityProviderModel);
}
@Override
@PreAuthorize("hasAuthority('" + WRITE_IDENTITY_PROVIDER_CONFIGURATION + "')")
public ResponseEntity<IdentityProviderModel> createIdentityProviderFromDescriptor(IdentityProviderWithDescriptorRequest identityProvider) {
Map<String, Object> requestMap = new HashMap<>();
requestMap.put("providerId", identityProvider.getProviderId());
requestMap.put("fromUrl", identityProvider.getSamlEntityDescriptorURL());
Map<String, String> configurationsMap = realmService.realm(getTenantId()).identityProviders().importFrom(requestMap);
if (configurationsMap == null || configurationsMap.isEmpty()) {
throw new BadRequestException("Could not set config from provided descriptor.");
}
configurationsMap.put("entityId", identityProvider.getEntityId());
IdentityProviderModel identityProviderModel = IdentityProviderMappingService.toModelFromDescriptorRequestAndConfig(identityProvider, configurationsMap);
return callKeyCloakIdentityProvidersCreateForModel(identityProviderModel);
}
private ResponseEntity<IdentityProviderModel> 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<IdentityProviderModel> 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<IdentityProviderModel> 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();
}
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -0,0 +1,10 @@
package com.knecon.fforesight.tenantusermanagement.model;
import java.util.Map;
public interface ExtensibleModel {
void setNotMappedFields(Map<String, String> notMappedFields);
Map<String, String> getNotMappedFields();
}

View File

@ -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<String, String> notMappedFields = new HashMap<>();
}

View File

@ -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;
}

View File

@ -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<IdentityProviderModel> identityProviders = new ArrayList<>();
}

View File

@ -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<String, String> notMappedFields = new HashMap<>();
public void setSpecificDefaults() {
this.setStoreToken(true);
this.setTrustEmail(true);
this.getConfig().setSyncMode(IdentityProviderSyncMode.FORCE);
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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";
}

View File

@ -0,0 +1,7 @@
package com.knecon.fforesight.tenantusermanagement.model;
public enum IdentityProviderSAMLSignatureKeyName {
NONE,
KEY_ID,
CERT_SUBJECT
}

View File

@ -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
}

View File

@ -0,0 +1,8 @@
package com.knecon.fforesight.tenantusermanagement.model;
public enum IdentityProviderSyncMode {
INHERIT,
IMPORT,
LEGACY,
FORCE
}

View File

@ -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;
}

View File

@ -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";
}

View File

@ -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<IdentityProviderRequest, IdentityProviderModel> 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<IdentityProviderModel, IdentityProviderRepresentation> getModelToRepresentationDeltaMapper() {
return ((identityProviderModel, identityProviderRepresentation) -> identityProviderRepresentation.setConfig(convertObjectToMap(identityProviderModel.getConfig())));
}
public static IdentityProviderModel toModelFromDescriptorRequestAndConfig(IdentityProviderWithDescriptorRequest identityProvider, Map<String, String> configurationsMap) {
IdentityProviderModel identityProviderModel = MagicConverter.convert(identityProvider, IdentityProviderModel.class);
identityProviderModel.setConfig(convertMapToObject(configurationsMap, IdentityProviderConfigModel.class));
return identityProviderModel;
}
@SneakyThrows
public static Map<String, String> convertObjectToMap(Object obj) {
Map<String, String> 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<IdentityProviderRepresentation, IdentityProviderModel> getRepresentationToModelDeltaMapper() {
return ((identityProviderRepresentation, identityProviderModel) -> identityProviderModel.setConfig(convertMapToObject(identityProviderRepresentation.getConfig(),
IdentityProviderConfigModel.class)));
}
@SneakyThrows
public static <T extends ExtensibleModel> T convertMapToObject(Map<String, String> map, Class<T> 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<String, String> 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 <T> 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);
}
}
}

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 {
}

View File

@ -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();
}
}

View File

@ -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