Compare commits

...

19 Commits

Author SHA1 Message Date
Dominique Eifländer
2cbc431606 Merge branch 'RED-9750-4.2' into 'release/1.134.x'
RED-9750: Added permissions to download packages created from support controller

See merge request fforesight/tenant-user-management-service!129
2024-09-12 09:27:12 +02:00
Dominique Eifländer
b7b2740760 RED-9750: Added permissions to download packages created from support controller 2024-09-12 09:21:05 +02:00
Maverick Studer
a8bbdca18e Merge branch 'RED-9872-bp' into 'release/1.134.x'
RED-9872: Tenant Management issues

See merge request fforesight/tenant-user-management-service!126
2024-08-26 14:52:54 +02:00
Maverick Studer
87e1493de9 RED-9872: Tenant Management issues 2024-08-26 14:52:53 +02:00
Maverick Studer
529669533e Merge branch 'RED-98720-bp' into 'release/1.134.x'
RED-9872: Tenant Management issues

See merge request fforesight/tenant-user-management-service!123
2024-08-15 09:44:53 +02:00
Maverick Studer
1ed01ce1ee RED-9872: Tenant Management issues 2024-08-15 09:44:53 +02:00
Kilian Schüttler
1ce5f3dabf Merge branch 'RED-9255-bp' into 'release/1.134.x'
RED-9255: add role for import files

See merge request fforesight/tenant-user-management-service!121
2024-07-25 14:57:04 +02:00
Kilian Schuettler
58be691b46 RED-9255: add role for import files 2024-07-25 14:51:12 +02:00
Christoph Schabert
665eea7393 Merge branch 'RED-9658-2' into 'release/1.134.x'
RED-9658: Fixed bug in mongo on the fly migration

See merge request fforesight/tenant-user-management-service!119
2024-07-15 15:50:26 +02:00
Dominique Eifländer
4d8cbfb2a7 RED-9658: Fixed bug in mongo on the fly migration 2024-07-15 15:43:00 +02:00
Dominique Eifländer
a248228b68 Merge branch 'RED-9658' into 'release/1.134.x'
RED-9658: Added mongoDb on the fly migration

See merge request fforesight/tenant-user-management-service!118
2024-07-15 11:50:58 +02:00
Dominique Eifländer
fc3e44b66e RED-9658: Added mongoDb on the fly migration 2024-07-15 11:40:55 +02:00
Maverick Studer
a1db318adc Merge branch 'RED-9628-bp' into 'release/1.134.x'
RED-9628: Some actions for users without roles not possible

See merge request fforesight/tenant-user-management-service!116
2024-07-10 11:52:02 +02:00
maverickstuder
d8c1121766 RED-9628: Some actions for users without roles not possible 2024-07-10 11:36:50 +02:00
Ali Oezyetimoglu
abc1702097 Merge branch 'RED-9512-bp' into 'release/1.134.x'
RED-9512: added null check for SMTP password when updating configuration

See merge request fforesight/tenant-user-management-service!119
2024-07-03 10:37:21 +02:00
Ali Oezyetimoglu
6b2ba15f7f RED-9512: added null check for SMTP password when updating configuration 2024-07-03 09:56:40 +02:00
Ali Oezyetimoglu
0c59fdf676 RED-9512: added null check for SMTP password when updating configuration
(cherry picked from commit aa9fe087f54eeaa6977d1f76616b10e21f447531)
2024-07-02 19:57:52 +02:00
Maverick Studer
5ad75932d8 Merge branch 'RED-8491-bp' into 'release/1.134.x'
RED-8491: Hide all KNECON_* roles for any possible access in all endpoints

See merge request fforesight/tenant-user-management-service!114
2024-06-27 09:52:44 +02:00
Maverick Studer
13ff6ced57 RED-8491: Hide all KNECON_* roles for any possible access in all endpoints 2024-06-27 09:52:44 +02:00
20 changed files with 656 additions and 273 deletions

View File

@ -18,7 +18,8 @@ import com.knecon.fforesight.tenantcommons.model.TenantResponse;
import com.knecon.fforesight.tenantcommons.model.UpdateDetailsRequest; import com.knecon.fforesight.tenantcommons.model.UpdateDetailsRequest;
import com.knecon.fforesight.tenantusermanagement.model.DeploymentKeyResponse; import com.knecon.fforesight.tenantusermanagement.model.DeploymentKeyResponse;
import com.knecon.fforesight.tenantusermanagement.model.SimpleTenantResponse; import com.knecon.fforesight.tenantusermanagement.model.SimpleTenantResponse;
import com.knecon.fforesight.tenantusermanagement.model.TenantRequest; import com.knecon.fforesight.tenantusermanagement.model.CreateTenantRequest;
import com.knecon.fforesight.tenantusermanagement.model.UpdateTenantRequest;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
@ -36,7 +37,7 @@ public interface TenantsResource {
@PostMapping(value = TENANTS_PATH, consumes = MediaType.APPLICATION_JSON_VALUE) @PostMapping(value = TENANTS_PATH, consumes = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Create a new tenant", description = "None") @Operation(summary = "Create a new tenant", description = "None")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")})
void createTenant(@RequestBody TenantRequest tenant); void createTenant(@RequestBody CreateTenantRequest tenant);
@ResponseBody @ResponseBody
@ -63,7 +64,7 @@ public interface TenantsResource {
@PutMapping(value = TENANTS_TENANT_ID_PATH, 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") @Operation(summary = "Update existing tenant", description = "None")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")})
TenantResponse updateTenant(@PathVariable("tenantId") String tenantId, @RequestBody TenantRequest tenantRequest); TenantResponse updateTenant(@PathVariable("tenantId") String tenantId, @RequestBody UpdateTenantRequest tenantRequest);
@PostMapping(value = TENANTS_TENANT_ID_PATH + "/details", consumes = MediaType.APPLICATION_JSON_VALUE) @PostMapping(value = TENANTS_TENANT_ID_PATH + "/details", consumes = MediaType.APPLICATION_JSON_VALUE)

View File

@ -10,7 +10,6 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
@ -61,7 +60,7 @@ public interface UserResource {
@ResponseBody @ResponseBody
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
@Operation(summary = "Update a user profile", description = "None") @Operation(summary = "Update a user profile", description = "None")
@ApiResponses(value = {@ApiResponse(responseCode = "204", description = "OK"), @ApiResponse(responseCode = "400", description = "Failed to update profile, e-mail invalid")}) @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "OK"), @ApiResponse(responseCode = "400", description = "Failed to update profile, e-mail invalid"), @ApiResponse(responseCode = "404", description = "The userId cannot be found.")})
@PostMapping(value = UPDATE_USER_PROFILE_PATH + USER_ID_PATH_VARIABLE, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @PostMapping(value = UPDATE_USER_PROFILE_PATH + USER_ID_PATH_VARIABLE, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
User updateProfile(@PathVariable(USER_ID) String userId, @RequestBody UpdateProfileRequest updateProfileRequest); User updateProfile(@PathVariable(USER_ID) String userId, @RequestBody UpdateProfileRequest updateProfileRequest);
@ -99,7 +98,7 @@ public interface UserResource {
@ResponseBody @ResponseBody
@Operation(summary = "Gets the user in realm including role info", description = "None") @Operation(summary = "Gets the user in realm including role info", description = "None")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "404", description = "The " + "userId cannot be found."), @ApiResponse(responseCode = "400", description = "The provided user id is empty or " + "null.")}) @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "404", description = "The userId cannot be found."), @ApiResponse(responseCode = "400", description = "The provided user id is empty or null.")})
@GetMapping(value = USER_REST_PATH + USER_ID_PATH_VARIABLE, produces = MediaType.APPLICATION_JSON_VALUE) @GetMapping(value = USER_REST_PATH + USER_ID_PATH_VARIABLE, produces = MediaType.APPLICATION_JSON_VALUE)
User getUserById(@PathVariable(USER_ID) String userId); User getUserById(@PathVariable(USER_ID) String userId);
@ -120,8 +119,7 @@ public interface UserResource {
@ResponseBody @ResponseBody
@Operation(summary = "Activate/deactivate a user profile", description = "None") @Operation(summary = "Activate/deactivate a user profile", description = "None")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "400", description = "Failed to activate/deactivate profile"), @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "400", description = "Failed to activate/deactivate profile"), @ApiResponse(responseCode = "403", description = "Cannot activate/deactivate users with higher rank roles"), @ApiResponse(responseCode = "404", description = "The userId cannot be found.")})
@ApiResponse(responseCode = "403", description = "Cannot activate/deactivate users with higher rank roles")})
@PostMapping(value = ACTIVATE_USER_PROFILE_PATH + USER_ID_PATH_VARIABLE, produces = MediaType.APPLICATION_JSON_VALUE) @PostMapping(value = ACTIVATE_USER_PROFILE_PATH + USER_ID_PATH_VARIABLE, produces = MediaType.APPLICATION_JSON_VALUE)
User activateProfile(@PathVariable(USER_ID) String userId, @RequestParam(IS_ACTIVE_PARAM) boolean isActive); User activateProfile(@PathVariable(USER_ID) String userId, @RequestParam(IS_ACTIVE_PARAM) boolean isActive);

View File

@ -16,7 +16,8 @@ import com.knecon.fforesight.tenantcommons.model.TenantResponse;
import com.knecon.fforesight.tenantcommons.model.UpdateDetailsRequest; import com.knecon.fforesight.tenantcommons.model.UpdateDetailsRequest;
import com.knecon.fforesight.tenantusermanagement.model.DeploymentKeyResponse; import com.knecon.fforesight.tenantusermanagement.model.DeploymentKeyResponse;
import com.knecon.fforesight.tenantusermanagement.model.SimpleTenantResponse; import com.knecon.fforesight.tenantusermanagement.model.SimpleTenantResponse;
import com.knecon.fforesight.tenantusermanagement.model.TenantRequest; import com.knecon.fforesight.tenantusermanagement.model.CreateTenantRequest;
import com.knecon.fforesight.tenantusermanagement.model.UpdateTenantRequest;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
@ -38,7 +39,7 @@ public interface InternalTenantsResource {
@PostMapping(value = "/tenants", consumes = MediaType.APPLICATION_JSON_VALUE) @PostMapping(value = "/tenants", consumes = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Creates a new Tenant", description = "None") @Operation(summary = "Creates a new Tenant", description = "None")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")})
TenantResponse createTenant(@RequestBody TenantRequest tenant); TenantResponse createTenant(@RequestBody CreateTenantRequest tenant);
@GetMapping(value = "/tenants", produces = MediaType.APPLICATION_JSON_VALUE) @GetMapping(value = "/tenants", produces = MediaType.APPLICATION_JSON_VALUE)
@ -56,7 +57,7 @@ public interface InternalTenantsResource {
@PutMapping(value = "/tenants/{tenantId}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @PutMapping(value = "/tenants/{tenantId}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "Update existing tenant", description = "None") @Operation(summary = "Update existing tenant", description = "None")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")})
TenantResponse updateTenant(@PathVariable("tenantId") String tenantId, @RequestBody TenantRequest tenantRequest); TenantResponse updateTenant(@PathVariable("tenantId") String tenantId, @RequestBody UpdateTenantRequest tenantRequest);
@GetMapping(value = "/tenants/simple", produces = MediaType.APPLICATION_JSON_VALUE) @GetMapping(value = "/tenants/simple", produces = MediaType.APPLICATION_JSON_VALUE)

View File

@ -57,7 +57,7 @@ public class SMTPConfigurationController implements SMTPConfigurationResource, P
var propertiesMap = convertSMTPConfigurationModelToMap(smtpConfigurationModel); var propertiesMap = convertSMTPConfigurationModelToMap(smtpConfigurationModel);
realmRepresentation.setSmtpServer(propertiesMap); realmRepresentation.setSmtpServer(propertiesMap);
if (!smtpConfigurationModel.getPassword().matches("\\**")) { if (smtpConfigurationModel.getPassword() != null && !smtpConfigurationModel.getPassword().matches("\\**")) {
realmRepresentation.getAttributesOrEmpty().put(SMTP_PASSWORD_KEY, encryptionDecryptionService.encrypt(smtpConfigurationModel.getPassword())); realmRepresentation.getAttributesOrEmpty().put(SMTP_PASSWORD_KEY, encryptionDecryptionService.encrypt(smtpConfigurationModel.getPassword()));
} }

View File

@ -23,7 +23,8 @@ import com.knecon.fforesight.tenantusermanagement.api.external.PublicResource;
import com.knecon.fforesight.tenantusermanagement.api.external.TenantsResource; import com.knecon.fforesight.tenantusermanagement.api.external.TenantsResource;
import com.knecon.fforesight.tenantusermanagement.model.DeploymentKeyResponse; import com.knecon.fforesight.tenantusermanagement.model.DeploymentKeyResponse;
import com.knecon.fforesight.tenantusermanagement.model.SimpleTenantResponse; import com.knecon.fforesight.tenantusermanagement.model.SimpleTenantResponse;
import com.knecon.fforesight.tenantusermanagement.model.TenantRequest; import com.knecon.fforesight.tenantusermanagement.model.CreateTenantRequest;
import com.knecon.fforesight.tenantusermanagement.model.UpdateTenantRequest;
import com.knecon.fforesight.tenantusermanagement.service.DeploymentKeyService; import com.knecon.fforesight.tenantusermanagement.service.DeploymentKeyService;
import com.knecon.fforesight.tenantusermanagement.service.TenantManagementService; import com.knecon.fforesight.tenantusermanagement.service.TenantManagementService;
@ -39,7 +40,7 @@ public class TenantsController implements TenantsResource, PublicResource {
@PreAuthorize("hasAuthority('" + CREATE_TENANT + "')") @PreAuthorize("hasAuthority('" + CREATE_TENANT + "')")
public void createTenant(@Valid @RequestBody TenantRequest tenantRequest) { public void createTenant(@Valid @RequestBody CreateTenantRequest tenantRequest) {
try { try {
tenantManagementService.createTenant(tenantRequest); tenantManagementService.createTenant(tenantRequest);
@ -83,7 +84,7 @@ public class TenantsController implements TenantsResource, PublicResource {
@PreAuthorize("hasAuthority('" + UPDATE_TENANT + "')") @PreAuthorize("hasAuthority('" + UPDATE_TENANT + "')")
public TenantResponse updateTenant(String tenantId, @RequestBody TenantRequest tenantRequest) { public TenantResponse updateTenant(String tenantId, @RequestBody UpdateTenantRequest tenantRequest) {
TenantResponse tenantResponse = tenantManagementService.updateTenant(tenantId, tenantRequest); TenantResponse tenantResponse = tenantManagementService.updateTenant(tenantId, tenantRequest);
return tenantManagementService.removePasswords(tenantResponse); return tenantManagementService.removePasswords(tenantResponse);

View File

@ -1,5 +1,6 @@
package com.knecon.fforesight.tenantusermanagement.controller.external; package com.knecon.fforesight.tenantusermanagement.controller.external;
import static com.knecon.fforesight.tenantusermanagement.permissions.ApplicationRoles.KNECON_ROLE_FILTER;
import static com.knecon.fforesight.tenantusermanagement.permissions.UserManagementPermissions.READ_ALL_USERS; import static com.knecon.fforesight.tenantusermanagement.permissions.UserManagementPermissions.READ_ALL_USERS;
import static com.knecon.fforesight.tenantusermanagement.permissions.UserManagementPermissions.READ_USERS; import static com.knecon.fforesight.tenantusermanagement.permissions.UserManagementPermissions.READ_USERS;
import static com.knecon.fforesight.tenantusermanagement.permissions.UserManagementPermissions.UPDATE_MY_PROFILE; import static com.knecon.fforesight.tenantusermanagement.permissions.UserManagementPermissions.UPDATE_MY_PROFILE;
@ -39,19 +40,6 @@ import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor @RequiredArgsConstructor
public class UserController implements UserResource, PublicResource { public class UserController implements UserResource, PublicResource {
public static final Predicate<User> KNECON_ROLE_FILTER = user -> {
Set<String> filteredRoles = user.getRoles()
.stream()
.filter(ApplicationRoles::isNoKneconRole)
.collect(Collectors.toSet());
if (!user.getRoles().isEmpty() && filteredRoles.isEmpty()) {
return false;
}
user.setRoles(filteredRoles);
return true;
};
private final UserService userService; private final UserService userService;
private final TenantUserManagementProperties tenantUserManagementProperties; private final TenantUserManagementProperties tenantUserManagementProperties;

View File

@ -9,16 +9,14 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.knecon.fforesight.tenantcommons.TenantContext;
import com.knecon.fforesight.tenantcommons.model.TenantResponse; import com.knecon.fforesight.tenantcommons.model.TenantResponse;
import com.knecon.fforesight.tenantcommons.model.UpdateDetailsRequest; import com.knecon.fforesight.tenantcommons.model.UpdateDetailsRequest;
import com.knecon.fforesight.tenantusermanagement.api.internal.InternalResource; import com.knecon.fforesight.tenantusermanagement.api.internal.InternalResource;
import com.knecon.fforesight.tenantusermanagement.api.internal.InternalTenantsResource; import com.knecon.fforesight.tenantusermanagement.api.internal.InternalTenantsResource;
import com.knecon.fforesight.tenantusermanagement.events.TenantCreatedEvent;
import com.knecon.fforesight.tenantusermanagement.events.TenantSyncEvent;
import com.knecon.fforesight.tenantusermanagement.model.DeploymentKeyResponse; import com.knecon.fforesight.tenantusermanagement.model.DeploymentKeyResponse;
import com.knecon.fforesight.tenantusermanagement.model.SimpleTenantResponse; import com.knecon.fforesight.tenantusermanagement.model.SimpleTenantResponse;
import com.knecon.fforesight.tenantusermanagement.model.TenantRequest; import com.knecon.fforesight.tenantusermanagement.model.CreateTenantRequest;
import com.knecon.fforesight.tenantusermanagement.model.UpdateTenantRequest;
import com.knecon.fforesight.tenantusermanagement.service.DeploymentKeyService; import com.knecon.fforesight.tenantusermanagement.service.DeploymentKeyService;
import com.knecon.fforesight.tenantusermanagement.service.TenantManagementService; import com.knecon.fforesight.tenantusermanagement.service.TenantManagementService;
@ -40,7 +38,7 @@ public class InternalTenantsController implements InternalTenantsResource, Inter
} }
public TenantResponse createTenant(@Valid @RequestBody TenantRequest tenantRequest) { public TenantResponse createTenant(@Valid @RequestBody CreateTenantRequest tenantRequest) {
try { try {
return tenantManagementService.createTenant(tenantRequest); return tenantManagementService.createTenant(tenantRequest);
@ -63,7 +61,7 @@ public class InternalTenantsController implements InternalTenantsResource, Inter
@Override @Override
public TenantResponse updateTenant(String tenantId, TenantRequest tenantRequest) { public TenantResponse updateTenant(String tenantId, UpdateTenantRequest tenantRequest) {
return tenantManagementService.updateTenant(tenantId, tenantRequest); return tenantManagementService.updateTenant(tenantId, tenantRequest);
} }

View File

@ -15,14 +15,12 @@ import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.StatementCallback; import org.springframework.jdbc.core.StatementCallback;
import org.springframework.jdbc.datasource.SingleConnectionDataSource; import org.springframework.jdbc.datasource.SingleConnectionDataSource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.knecon.fforesight.tenantcommons.model.AzureStorageConnection; import com.knecon.fforesight.tenantcommons.model.AzureStorageConnection;
import com.knecon.fforesight.tenantcommons.model.DatabaseConnection; import com.knecon.fforesight.tenantcommons.model.DatabaseConnection;
import com.knecon.fforesight.tenantcommons.model.S3StorageConnection; import com.knecon.fforesight.tenantcommons.model.S3StorageConnection;
import com.knecon.fforesight.tenantcommons.model.SearchConnection;
import com.knecon.fforesight.tenantusermanagement.model.SearchConnectionRequest; import com.knecon.fforesight.tenantusermanagement.model.SearchConnectionRequest;
import com.knecon.fforesight.tenantusermanagement.model.TenantRequest; import com.knecon.fforesight.tenantusermanagement.model.CreateTenantRequest;
import com.knecon.fforesight.tenantusermanagement.model.TenantUser; import com.knecon.fforesight.tenantusermanagement.model.TenantUser;
import com.knecon.fforesight.tenantusermanagement.repository.TenantRepository; import com.knecon.fforesight.tenantusermanagement.repository.TenantRepository;
import com.knecon.fforesight.tenantusermanagement.service.TenantManagementService; import com.knecon.fforesight.tenantusermanagement.service.TenantManagementService;
@ -74,7 +72,7 @@ public class DevTestTenantService {
createDatabase(tenantsDBName, tenantsDBPassword); createDatabase(tenantsDBName, tenantsDBPassword);
createSchema(jdbcUrl, tenantId, tenantsDBName, tenantsDBPassword); createSchema(jdbcUrl, tenantId, tenantsDBName, tenantsDBPassword);
var tenantRequest = TenantRequest.builder() var tenantRequest = CreateTenantRequest.builder()
.tenantId(tenantId) .tenantId(tenantId)
.displayName(tenantId) .displayName(tenantId)
.guid(UUID.randomUUID().toString()) .guid(UUID.randomUUID().toString())

View File

@ -20,8 +20,8 @@ import lombok.NoArgsConstructor;
@Builder @Builder
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
@Schema(description = "Object containing the request to create or update a tenant.") @Schema(description = "Object containing the request to create a tenant.")
public class TenantRequest { public class CreateTenantRequest {
@NotBlank @NotBlank
@Pattern(regexp = "[A-Za-z0-9_-]*", message = "Tenant Id must match [A-Za-z0-9_-]") @Pattern(regexp = "[A-Za-z0-9_-]*", message = "Tenant Id must match [A-Za-z0-9_-]")

View File

@ -0,0 +1,43 @@
package com.knecon.fforesight.tenantusermanagement.model;
import java.util.ArrayList;
import java.util.List;
import com.knecon.fforesight.tenantcommons.model.AzureStorageConnection;
import com.knecon.fforesight.tenantcommons.model.DatabaseConnection;
import com.knecon.fforesight.tenantcommons.model.MongoDBConnection;
import com.knecon.fforesight.tenantcommons.model.S3StorageConnection;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "Object containing the request to update a tenant.")
public class UpdateTenantRequest {
@NotBlank
@Schema(description = "Parameter containing the display name of the tenant.")
private String displayName;
@Schema(description = "Parameter containing the global unique ID of the tenant.")
private String guid;
@Schema(description = "Parameter containing data of the database connection.")
private DatabaseConnection databaseConnection;
@Schema(description = "Parameter containing data of the search connection.")
private SearchConnectionRequest searchConnection;
@Schema(description = "Parameter containing data of the Azure storage connection.")
private AzureStorageConnection azureStorageConnection;
@Schema(description = "Parameter containing data of the S3 storage connection.")
private S3StorageConnection s3StorageConnection;
@Schema(description = "Parameter containing data of the MongoDB connection.")
private MongoDBConnection mongoDBConnection;
}

View File

@ -1,6 +1,10 @@
package com.knecon.fforesight.tenantusermanagement.permissions; package com.knecon.fforesight.tenantusermanagement.permissions;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import com.knecon.fforesight.tenantusermanagement.model.User;
public final class ApplicationRoles { public final class ApplicationRoles {
@ -15,6 +19,21 @@ public final class ApplicationRoles {
public static final Set<String> RED_ROLES = Set.of(RED_USER_ROLE, RED_MANAGER_ROLE, RED_ADMIN_ROLE, RED_USER_ADMIN_ROLE); public static final Set<String> RED_ROLES = Set.of(RED_USER_ROLE, RED_MANAGER_ROLE, RED_ADMIN_ROLE, RED_USER_ADMIN_ROLE);
public static final Predicate<User> KNECON_ROLE_FILTER = user -> {
Set<String> filteredRoles = user.getRoles()
.stream()
.filter(ApplicationRoles::isNoKneconRole)
.collect(Collectors.toSet());
if (!user.getRoles().isEmpty() && filteredRoles.isEmpty()) {
return false;
}
user.setRoles(filteredRoles);
return true;
};
public static boolean isNoKneconRole(String role) { public static boolean isNoKneconRole(String role) {
return !KNECON_ROLES.contains(role); return !KNECON_ROLES.contains(role);

View File

@ -0,0 +1,120 @@
package com.knecon.fforesight.tenantusermanagement.service;
import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.BsonString;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.knecon.fforesight.tenantcommons.EncryptionDecryptionService;
import com.knecon.fforesight.tenantcommons.model.MongoDBConnection;
import com.knecon.fforesight.tenantcommons.utils.MongoConnectionStringHelper;
import com.knecon.fforesight.tenantusermanagement.entity.MongoDBConnectionEntity;
import com.knecon.fforesight.tenantusermanagement.entity.TenantEntity;
import com.knecon.fforesight.tenantusermanagement.properties.TenantUserManagementProperties;
import com.knecon.fforesight.tenantusermanagement.repository.TenantRepository;
import com.mongodb.MongoCommandException;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoDatabase;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
// This is just for migration from 4.0 to 4.1 and can be removed afterward.
@Service
@RequiredArgsConstructor
public class MongoDbOntheFlyMigrationService {
private final EncryptionDecryptionService encryptionService;
private final TenantUserManagementProperties tenantUserManagementProperties;
private final TenantRepository tenantRepository;
@Value("${default.mongodb.username:}")
private String defaultMongoDBUsername;
@Value("${default.mongodb.password:}")
private String defaultMongoDBPassword;
@Value("${default.mongodb.address:}")
private String defaultMongoDBAddress;
@Value("${default.mongodb.prefix:}")
private String defaultMongoDBPrefix;
@Value("${default.mongodb.options:}")
private String defaultMongoDBOptions;
@Transactional
public TenantEntity updateMongoDatabaseConnection(TenantEntity tenantEntity) {
if ((tenantEntity.getMongoDBConnection() == null || tenantEntity.getMongoDBConnection().getUsername() == null) && defaultMongoDBUsername != null && !defaultMongoDBUsername.isEmpty()) {
tenantEntity.setMongoDBConnection(MongoDBConnectionEntity.builder()
.prefix(defaultMongoDBPrefix)
.username(defaultMongoDBUsername)
.password(encryptionService.encrypt(defaultMongoDBPassword))
.address(defaultMongoDBAddress)
.database(buildIndexPrefix(tenantEntity.getTenantId()))
.options(defaultMongoDBOptions)
.build());
tenantRepository.save(tenantEntity);
createMongoDBDatabase(convert(tenantEntity.getMongoDBConnection()));
}
return tenantEntity;
}
private String buildIndexPrefix(String tenantId) {
return tenantUserManagementProperties.getAppPrefix() + "_" + tenantId;
}
private MongoDBConnection convert(MongoDBConnectionEntity mongoDBConnectionEntity) {
return MongoDBConnection.builder()
.prefix(mongoDBConnectionEntity.getPrefix())
.username(mongoDBConnectionEntity.getUsername())
.password(mongoDBConnectionEntity.getPassword())
.address(mongoDBConnectionEntity.getAddress())
.database(mongoDBConnectionEntity.getDatabase())
.options(mongoDBConnectionEntity.getOptions())
.build();
}
private void createMongoDBDatabase(MongoDBConnection mongoDBConnection) {
mongoDBConnection.setPassword(encryptionService.decrypt(mongoDBConnection.getPassword()));
try (MongoClient mongoClient = MongoClients.create(MongoConnectionStringHelper.buildGenericMongoConnectionString(mongoDBConnection))) {
String databaseName = mongoDBConnection.getDatabase();
String username = mongoDBConnection.getUsername();
MongoDatabase database = mongoClient.getDatabase(databaseName);
BsonDocument createUserCommand = new BsonDocument();
createUserCommand.append("createUser", new BsonString(username));
createUserCommand.append("pwd", new BsonString(mongoDBConnection.getPassword()));
BsonArray roles = new BsonArray();
roles.add(new BsonDocument("role", new BsonString("dbOwner")).append("db", new BsonString(databaseName)));
createUserCommand.append("roles", roles);
try {
database.runCommand(createUserCommand);
} catch (MongoCommandException mongoCommandException) {
// ignore user already exists (51003) because of possibly already created users being present
// and command not supported (115) because of azure deployment having a different user management
if (mongoCommandException.getErrorCode() != 51003 && mongoCommandException.getErrorCode() != 115) {
throw mongoCommandException;
}
}
}
}
}

View File

@ -19,6 +19,7 @@ import org.apache.commons.lang3.StringUtils;
import org.bson.BsonArray; import org.bson.BsonArray;
import org.bson.BsonDocument; import org.bson.BsonDocument;
import org.bson.BsonString; import org.bson.BsonString;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
@ -62,8 +63,9 @@ import com.knecon.fforesight.tenantusermanagement.entity.TenantEntity;
import com.knecon.fforesight.tenantusermanagement.events.TenantCreatedEvent; import com.knecon.fforesight.tenantusermanagement.events.TenantCreatedEvent;
import com.knecon.fforesight.tenantusermanagement.events.TenantSyncEvent; import com.knecon.fforesight.tenantusermanagement.events.TenantSyncEvent;
import com.knecon.fforesight.tenantusermanagement.model.SearchConnectionRequest; import com.knecon.fforesight.tenantusermanagement.model.SearchConnectionRequest;
import com.knecon.fforesight.tenantusermanagement.model.TenantRequest; import com.knecon.fforesight.tenantusermanagement.model.CreateTenantRequest;
import com.knecon.fforesight.tenantusermanagement.model.TenantUser; import com.knecon.fforesight.tenantusermanagement.model.TenantUser;
import com.knecon.fforesight.tenantusermanagement.model.UpdateTenantRequest;
import com.knecon.fforesight.tenantusermanagement.properties.TenantUserManagementProperties; import com.knecon.fforesight.tenantusermanagement.properties.TenantUserManagementProperties;
import com.knecon.fforesight.tenantusermanagement.repository.TenantRepository; import com.knecon.fforesight.tenantusermanagement.repository.TenantRepository;
import com.knecon.fforesight.tenantusermanagement.utils.JDBCUtils; import com.knecon.fforesight.tenantusermanagement.utils.JDBCUtils;
@ -104,23 +106,20 @@ public class TenantManagementService implements TenantProvider {
private final RealmService realmService; private final RealmService realmService;
private final RabbitTemplate rabbitTemplate; private final RabbitTemplate rabbitTemplate;
private final StorageConfiguration storageConfiguration; private final StorageConfiguration storageConfiguration;
private final MongoDbOntheFlyMigrationService mongoDbOntheFlyMigrationService;
@Value("${fforesight.tenant-exchange.name}") @Value("${fforesight.tenant-exchange.name}")
private String tenantExchangeName; private String tenantExchangeName;
@SneakyThrows @SneakyThrows
public TenantResponse createTenant(TenantRequest tenantRequest) { public TenantResponse createTenant(CreateTenantRequest tenantRequest) {
// For now we update the master realm theme whenever we create the tenant // For now we update the master realm theme whenever we create the tenant
updateMasterTheme(tenantUserManagementProperties.getLoginTheme()); updateMasterTheme(tenantUserManagementProperties.getLoginTheme());
updateMasterDisplayName(tenantUserManagementProperties.getApplicationName()); updateMasterDisplayName(tenantUserManagementProperties.getApplicationName());
log.info("Tenants are: {}", log.info("Tenants are: {}", tenantRepository.findAll().stream().map(TenantEntity::getTenantId).toList());
tenantRepository.findAll()
.stream()
.map(TenantEntity::getTenantId)
.toList());
log.info("Requested to create tenant for: {}", tenantRequest.getTenantId()); log.info("Requested to create tenant for: {}", tenantRequest.getTenantId());
try { try {
@ -218,6 +217,7 @@ public class TenantManagementService implements TenantProvider {
log.info("Dispatched message for tenant: {}", tenantRequest.getTenantId()); log.info("Dispatched message for tenant: {}", tenantRequest.getTenantId());
return convert(saved); return convert(saved);
} else { } else {
throw new ResponseStatusException(HttpStatus.CONFLICT, "Tenant exists"); throw new ResponseStatusException(HttpStatus.CONFLICT, "Tenant exists");
} }
@ -303,9 +303,8 @@ public class TenantManagementService implements TenantProvider {
private void propagateTenantToKeyCloak(String tenantId, List<TenantUser> usersToCreate) throws InterruptedException { private void propagateTenantToKeyCloak(String tenantId, List<TenantUser> usersToCreate) throws InterruptedException {
log.info("Creating realm for tenant: {}", tenantId); log.info("Creating or updating realm for tenant: {}", tenantId);
createOrUpdateRealm(tenantId, usersToCreate); createOrUpdateRealm(tenantId, usersToCreate);
log.info("Created realm for tenant: {}", tenantId);
var waitTime = 0; var waitTime = 0;
boolean realmReady; boolean realmReady;
@ -321,7 +320,7 @@ public class TenantManagementService implements TenantProvider {
} while (waitTime < MAX_WAIT_TIME); } while (waitTime < MAX_WAIT_TIME);
if (!realmReady) { if (!realmReady) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to create KC realm"); throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to create or update KC realm");
} }
setPasswordPolicyForRealm(tenantId); setPasswordPolicyForRealm(tenantId);
@ -349,7 +348,7 @@ public class TenantManagementService implements TenantProvider {
} }
private void createSchema(TenantRequest tenantRequest) { private void createSchema(CreateTenantRequest tenantRequest) {
var jdbcUrl = JDBCUtils.buildJdbcUrl(tenantRequest.getDatabaseConnection()); var jdbcUrl = JDBCUtils.buildJdbcUrl(tenantRequest.getDatabaseConnection());
try (Connection connection = DriverManager.getConnection(jdbcUrl, try (Connection connection = DriverManager.getConnection(jdbcUrl,
@ -387,7 +386,7 @@ public class TenantManagementService implements TenantProvider {
} }
private void createMongoDBDatabase(TenantRequest tenant) { private void createMongoDBDatabase(CreateTenantRequest tenant) {
MongoDBConnection mongoDBConnection = tenant.getMongoDBConnection(); MongoDBConnection mongoDBConnection = tenant.getMongoDBConnection();
try (MongoClient mongoClient = MongoClients.create(MongoConnectionStringHelper.buildGenericMongoConnectionString(mongoDBConnection))) { try (MongoClient mongoClient = MongoClients.create(MongoConnectionStringHelper.buildGenericMongoConnectionString(mongoDBConnection))) {
@ -430,7 +429,8 @@ public class TenantManagementService implements TenantProvider {
public void createOrUpdateRealm(String tenantId, List<TenantUser> users) { public void createOrUpdateRealm(String tenantId, List<TenantUser> users) {
if (syncRealmIfExists(tenantId)) { if (syncRealmIfExists(tenantId, users)) {
log.info("Updated realm for tenant: {}", tenantId);
return; return;
} }
@ -446,12 +446,12 @@ public class TenantManagementService implements TenantProvider {
realm.setRoles(getRealmRoles()); realm.setRoles(getRealmRoles());
if (users != null) { if (users != null) {
realm.setUsers(users.stream() realm.setUsers(users.stream().map(this::toUserRepresentation).toList());
.map(this::toUserRepresentation)
.toList());
} }
keycloak.getAdminClient().realms().create(realm); keycloak.getAdminClient().realms().create(realm);
log.info("Created realm for tenant: {}", tenantId);
} }
@ -459,7 +459,7 @@ public class TenantManagementService implements TenantProvider {
try { try {
log.info("Deleting existing realms for tenant: {}", tenantId); log.info("Deleting existing realms for tenant: {}", tenantId);
keycloak.getAdminClient().realm(tenantId).remove(); getRealmResource(tenantId).remove();
} catch (Exception e) { } catch (Exception e) {
log.warn("Could not delete realm:", e); log.warn("Could not delete realm:", e);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Tenant deletion failed: " + e.getMessage(), e); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Tenant deletion failed: " + e.getMessage(), e);
@ -468,10 +468,10 @@ public class TenantManagementService implements TenantProvider {
} }
private boolean syncRealmIfExists(String tenantId) { private boolean syncRealmIfExists(String tenantId, List<TenantUser> users) {
try { try {
var existingRealm = keycloak.getAdminClient().realm(tenantId).toRepresentation(); var existingRealm = getRealmResource(tenantId).toRepresentation();
if (existingRealm != null) { if (existingRealm != null) {
log.info("Updating existing realm: {}", tenantId); log.info("Updating existing realm: {}", tenantId);
existingRealm.setLoginTheme(tenantUserManagementProperties.getDefaultTheme()); existingRealm.setLoginTheme(tenantUserManagementProperties.getDefaultTheme());
@ -480,23 +480,40 @@ public class TenantManagementService implements TenantProvider {
existingRealm.setAccessTokenLifespan(tenantUserManagementProperties.getAccessTokenLifeSpan()); existingRealm.setAccessTokenLifespan(tenantUserManagementProperties.getAccessTokenLifeSpan());
existingRealm.setSsoSessionIdleTimeout(tenantUserManagementProperties.getSsoSessionIdleTimeout()); existingRealm.setSsoSessionIdleTimeout(tenantUserManagementProperties.getSsoSessionIdleTimeout());
var clients = getRealmClients(); var clients = getRealmClients();
var relevantClientNames = clients.stream() var relevantClientNames = clients.stream().map(c -> c.getClientId().toLowerCase(Locale.getDefault())).collect(Collectors.toSet());
.map(c -> c.getClientId().toLowerCase(Locale.getDefault())) var existingClients = getRealmResource(tenantId).clients().findAll();
.collect(Collectors.toSet());
var existingClients = keycloak.getAdminClient().realm(tenantId).clients().findAll();
existingClients.forEach(ec -> { existingClients.forEach(ec -> {
if (relevantClientNames.contains(ec.getClientId().toLowerCase(Locale.getDefault()))) { if (relevantClientNames.contains(ec.getClientId().toLowerCase(Locale.getDefault()))) {
log.info("Removing client: {}", ec.getName()); log.info("Removing client: {}", ec.getName());
keycloak.getAdminClient().realm(tenantId).clients() getRealmResource(tenantId).clients().get(ec.getId()).remove();
.get(ec.getId()).remove();
} }
}); });
clients.forEach(c -> keycloak.getAdminClient().realm(tenantId).clients().create(c)); clients.forEach(c -> getRealmResource(tenantId).clients().create(c));
existingRealm.setClients(clients); existingRealm.setClients(clients);
existingRealm.setRoles(getRealmRoles()); existingRealm.setRoles(getRealmRoles());
keycloak.getAdminClient().realm(tenantId).update(existingRealm);
if (users != null) {
var userRepresentationlist = users.stream()
.map(this::toUserRepresentation)
.toList();
List<UserRepresentation> toUpdate = userRepresentationlist.stream()
.filter(existingRealm.getUsers()::contains)
.toList();
var toAdd = new ArrayList<>(userRepresentationlist);
toAdd.removeAll(toUpdate);
toAdd.forEach(user -> getRealmResource(tenantId).users().create(user));
toUpdate.forEach(user -> getRealmResource(tenantId).users().searchByUsername(user.getUsername(), true)
.stream()
.findFirst()
.ifPresent(userRepresentation -> {
getRealmResource(tenantId).users().get(userRepresentation.getId()).update(user);
}));
existingRealm.getUsers().addAll(toAdd);
} getRealmResource(tenantId).update(existingRealm);
return true; return true;
} }
} catch (NotFoundException e) { } catch (NotFoundException e) {
@ -504,8 +521,13 @@ public class TenantManagementService implements TenantProvider {
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to update realm: {}", tenantId, e); log.warn("Failed to update realm: {}", tenantId, e);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Failed to update realm: " + tenantId); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Failed to update realm: " + tenantId);
} return false;
} }
return false;
private RealmResource getRealmResource(String tenantId) {
return keycloak.getAdminClient().realm(tenantId);
} }
@ -598,9 +620,7 @@ public class TenantManagementService implements TenantProvider {
private boolean tryToAccessRealm(String tenantId) { private boolean tryToAccessRealm(String tenantId) {
try { try {
return keycloak.getAdminClient().realms().findAll() return keycloak.getAdminClient().realms().findAll().stream().anyMatch(r -> r.getRealm().equals(tenantId));
.stream()
.anyMatch(r -> r.getRealm().equals(tenantId));
} catch (Exception e) { } catch (Exception e) {
return false; return false;
} }
@ -621,7 +641,7 @@ public class TenantManagementService implements TenantProvider {
user.setLastName(redUser.getLastName()); user.setLastName(redUser.getLastName());
user.setEmailVerified(true); user.setEmailVerified(true);
var roles = new ArrayList<String>(redUser.getRoles() != null ? redUser.getRoles() : new ArrayList<>()); var roles = new ArrayList<>(redUser.getRoles() != null ? redUser.getRoles() : new ArrayList<>());
roles.add("uma_authorization"); roles.add("uma_authorization");
roles.add("offline_access"); roles.add("offline_access");
@ -656,13 +676,14 @@ public class TenantManagementService implements TenantProvider {
public TenantResponse getTenant(String tenantId) { public TenantResponse getTenant(String tenantId) {
return tenantRepository.findById(tenantId) var tenantEntity = tenantRepository.findById(tenantId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Tenant does not exist"));
.map(this::convert)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Tenant does not exist")); return convert(mongoDbOntheFlyMigrationService.updateMongoDatabaseConnection(tenantEntity));
} }
public TenantResponse updateTenant(String tenantId, TenantRequest tenantRequest) { @SneakyThrows
public TenantResponse updateTenant(String tenantId, UpdateTenantRequest tenantRequest) {
if (tenantRequest.getS3StorageConnection() != null && tenantRequest.getAzureStorageConnection() != null) { if (tenantRequest.getS3StorageConnection() != null && tenantRequest.getAzureStorageConnection() != null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Not possible to set both azure and s3 connection, please only specify one"); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Not possible to set both azure and s3 connection, please only specify one");
@ -746,7 +767,20 @@ public class TenantManagementService implements TenantProvider {
.build()); .build());
} }
return convert(tenantRepository.save(tenantEntity)); propagateTenantToKeyCloak(tenantId, null);
TenantResponse tenantResponse = convert(tenantRepository.save(tenantEntity));
log.info("Persisted tenant update: {}", tenantId);
TenantContext.setTenantId(tenantEntity.getTenantId());
rabbitTemplate.convertAndSend(tenantExchangeName, "tenant.updated", tenantResponse);
TenantContext.clear();
log.info("Dispatched message for tenant: {}", tenantId);
return tenantResponse;
} else { } else {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Tenant does not exist"); throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Tenant does not exist");
} }
@ -755,10 +789,7 @@ public class TenantManagementService implements TenantProvider {
public List<TenantResponse> getTenants() { public List<TenantResponse> getTenants() {
return tenantRepository.findAll() return tenantRepository.findAll().stream().map(mongoDbOntheFlyMigrationService::updateMongoDatabaseConnection).map(this::convert).toList();
.stream()
.map(this::convert)
.toList();
} }
@ -895,7 +926,7 @@ public class TenantManagementService implements TenantProvider {
public void syncTenant(String tenantId, JsonNode payload) { public void syncTenant(String tenantId, JsonNode payload) {
log.info("Syncing Realm: {}", tenantId); log.info("Syncing Realm: {}", tenantId);
syncRealmIfExists(tenantId); syncRealmIfExists(tenantId, null);
setPasswordPolicyForRealm(tenantId); setPasswordPolicyForRealm(tenantId);
generalConfigurationService.initGeneralConfiguration(tenantId); generalConfigurationService.initGeneralConfiguration(tenantId);
keyCloakRoleManagerService.updateRoles(tenantId); keyCloakRoleManagerService.updateRoles(tenantId);

View File

@ -166,9 +166,9 @@ public class UserService {
var currentUserRoles = this.getUserRoles(KeycloakSecurity.getUserId()); var currentUserRoles = this.getUserRoles(KeycloakSecurity.getUserId());
if (!userId.equalsIgnoreCase(KeycloakSecurity.getUserId()) && roles.stream() Set<String> oldRoles = getRoles(userId);
.anyMatch(ApplicationRoles::isKneconRole) && currentUserRoles.stream() if (!userId.equalsIgnoreCase(KeycloakSecurity.getUserId()) && !oldRoles.isEmpty() && oldRoles.stream()
.noneMatch(ApplicationRoles::isKneconRole)) { .allMatch(ApplicationRoles::isKneconRole)) {
throw new NotFoundException("User with id: " + userId + " does not exist"); throw new NotFoundException("User with id: " + userId + " does not exist");
} }
@ -182,19 +182,18 @@ public class UserService {
var allRoles = tenantUserManagementProperties.getKcRoleMapping().getAllRoles(); var allRoles = tenantUserManagementProperties.getKcRoleMapping().getAllRoles();
newRoles.forEach(role -> { newRoles.forEach(role -> {
if (!allRoles.contains(role) || ApplicationRoles.isKneconRole(role) && currentUserRoles.stream() if (!allRoles.contains(role) || ApplicationRoles.isKneconRole(role)) {
.noneMatch(ApplicationRoles::isKneconRole)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid role: " + role); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid role: " + role);
} }
}); });
var userResource = getUserResource(userId); var userResource = getUserResource(userId);
var userRoles = userResource.roles().realmLevel().listEffective() var oldRoles = userResource.roles().realmLevel().listEffective()
.stream() .stream()
.map(RoleRepresentation::getName) .map(RoleRepresentation::getName)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
validateSufficientRoles(userId, userRoles, newRoles, currentUserRoles); validateSufficientRoles(userId, oldRoles, newRoles, currentUserRoles);
var currentRolesAsRoleRepresentation = allRoles.stream() var currentRolesAsRoleRepresentation = allRoles.stream()
.map(this::getRoleRepresentation) .map(this::getRoleRepresentation)
@ -207,12 +206,33 @@ public class UserService {
userResource.roles().realmLevel().add(newMappedRoles); userResource.roles().realmLevel().add(newMappedRoles);
var userWithNewRoles = getUserByUsername(userResource.toRepresentation().getUsername()); var userWithNewRoles = getUserByUsername(userResource.toRepresentation().getUsername());
this.rabbitTemplate.convertAndSend(userExchangeName, "user.rolesUpdated", (new UserRolesUpdatedEvent(userWithNewRoles, userRoles, newRoles, KeycloakSecurity.getUserId()))); this.rabbitTemplate.convertAndSend(userExchangeName, "user.rolesUpdated", (new UserRolesUpdatedEvent(userWithNewRoles, oldRoles, newRoles, KeycloakSecurity.getUserId())));
return userWithNewRoles; return userWithNewRoles;
} }
@CacheEvict(value = "${commons.keycloak.userCache}", allEntries = true, beforeInvocation = true)
public void removeRolesForDeletion(String userId, Set<String> roles) {
var allRoles = tenantUserManagementProperties.getKcRoleMapping().getAllRoles();
roles.forEach(role -> {
if (!allRoles.contains(role)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid role: " + role);
}
});
var userResource = getUserResource(userId);
var currentRolesAsRoleRepresentation = roles.stream()
.map(this::getRoleRepresentation)
.collect(Collectors.toList());
userResource.roles().realmLevel().remove(currentRolesAsRoleRepresentation);
}
@CacheEvict(value = "${commons.keycloak.userCache}", allEntries = true, beforeInvocation = true) @CacheEvict(value = "${commons.keycloak.userCache}", allEntries = true, beforeInvocation = true)
public void validateSufficientRoles(String userId, Set<String> userRoles, Set<String> newRoles, Set<String> currentUserRoles) { public void validateSufficientRoles(String userId, Set<String> userRoles, Set<String> newRoles, Set<String> currentUserRoles) {
@ -231,7 +251,7 @@ public class UserService {
var untouchableRoles = userRoles.stream() var untouchableRoles = userRoles.stream()
.filter(roleMapping::isValidRole) .filter(roleMapping::isValidRole)
.map(roleMapping::getRole) .map(roleMapping::getRole)
.filter(r -> r.getRank() > maxRank) .filter(r -> r.getRank() > maxRank || ApplicationRoles.isKneconRole(r.getName()))
.map(KCRole::getName) .map(KCRole::getName)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
@ -288,6 +308,10 @@ public class UserService {
} }
var updatedProfile = getUserByUsername(userRepresentation.getUsername()); var updatedProfile = getUserByUsername(userRepresentation.getUsername());
updatedProfile.setRoles(updatedProfile.getRoles()
.stream()
.filter(ApplicationRoles::isNoKneconRole)
.collect(Collectors.toSet()));
this.rabbitTemplate.convertAndSend(userExchangeName, "user.ownProfileUpdated", (new UserUpdatedOwnProfileEvent(updatedProfile))); this.rabbitTemplate.convertAndSend(userExchangeName, "user.ownProfileUpdated", (new UserUpdatedOwnProfileEvent(updatedProfile)));
@ -419,18 +443,25 @@ public class UserService {
return; return;
} }
var status = validateExecution(KeycloakSecurity.getUserId(), userId); var status = validateExecutionForDeletion(KeycloakSecurity.getUserId(), userId);
if (status.equals(ValidationStatus.FORBIDDEN)) { if (status.equals(ValidationResult.FORBIDDEN)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "It is not allowed to delete a user with higher ranking roles"); throw new ResponseStatusException(HttpStatus.FORBIDDEN, "It is not allowed to delete a user with higher ranking roles");
} else if (status.equals(ValidationStatus.INVALID)) { } else if (status.equals(ValidationResult.INVALID)) {
return; return;
} }
var userResource = getUserResource(userId); var userResource = getUserResource(userId);
var userToBeRemoved = getUserByUsername(userResource.toRepresentation().getUsername()); var userToBeRemoved = getUserByUsername(userResource.toRepresentation().getUsername());
if (status.equals(ValidationResult.ROLE_REMOVAL)) {
removeRolesForDeletion(userId,
getRoles(userId).stream()
.filter(ApplicationRoles::isNoKneconRole)
.collect(Collectors.toSet()));
} else {
userResource.remove(); userResource.remove();
}
this.rabbitTemplate.convertAndSend(userExchangeName, "user.deleted", (new UserRemovedEvent(userToBeRemoved, KeycloakSecurity.getUserId()))); this.rabbitTemplate.convertAndSend(userExchangeName, "user.deleted", (new UserRemovedEvent(userToBeRemoved, KeycloakSecurity.getUserId())));
@ -443,8 +474,9 @@ public class UserService {
var user = this.getUserResource(userId); var user = this.getUserResource(userId);
var userRepresentation = user.toRepresentation(); var userRepresentation = user.toRepresentation();
if (getRoles(userId).stream() Set<String> currentRoles = getRoles(userId);
.anyMatch(ApplicationRoles::isKneconRole)) { if (!userExists(userId) || !currentRoles.isEmpty() && currentRoles.stream()
.allMatch(ApplicationRoles::isKneconRole)) {
throw new NotFoundException("User with id: " + userId + " does not exist"); throw new NotFoundException("User with id: " + userId + " does not exist");
} }
@ -467,6 +499,10 @@ public class UserService {
setRoles(userId, updateProfileRequest.getRoles()); setRoles(userId, updateProfileRequest.getRoles());
var updatedUser = getUserByUsername(userRepresentation.getUsername()); var updatedUser = getUserByUsername(userRepresentation.getUsername());
updatedUser.setRoles(updatedUser.getRoles()
.stream()
.filter(ApplicationRoles::isNoKneconRole)
.collect(Collectors.toSet()));
this.rabbitTemplate.convertAndSend(userExchangeName, "user.updated", (new UserUpdatedEvent(updatedUser, KeycloakSecurity.getUserId()))); this.rabbitTemplate.convertAndSend(userExchangeName, "user.updated", (new UserUpdatedEvent(updatedUser, KeycloakSecurity.getUserId())));
@ -479,9 +515,9 @@ public class UserService {
if (!userId.equalsIgnoreCase(KeycloakSecurity.getUserId())) { if (!userId.equalsIgnoreCase(KeycloakSecurity.getUserId())) {
var status = validateExecution(KeycloakSecurity.getUserId(), userId); var status = validateExecution(KeycloakSecurity.getUserId(), userId);
if (status.equals(ValidationStatus.FORBIDDEN)) { if (status.equals(ValidationResult.FORBIDDEN)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "It is not allowed to activate/deactivate a user with higher ranking roles"); throw new ResponseStatusException(HttpStatus.FORBIDDEN, "It is not allowed to activate/deactivate a user with higher ranking roles");
} else if (status.equals(ValidationStatus.INVALID)) { } else if (status.equals(ValidationResult.INVALID)) {
throw new NotFoundException("User with id: " + userId + " does not exist"); throw new NotFoundException("User with id: " + userId + " does not exist");
} }
} }
@ -499,11 +535,14 @@ public class UserService {
} }
var toggledUser = getUserByUsername(userRepresentation.getUsername()); var toggledUser = getUserByUsername(userRepresentation.getUsername());
toggledUser.setRoles(toggledUser.getRoles()
.stream()
.filter(ApplicationRoles::isNoKneconRole)
.collect(Collectors.toSet()));
this.rabbitTemplate.convertAndSend(userExchangeName, "user.statusChanged", (new UserStatusToggleEvent(toggledUser, KeycloakSecurity.getUserId()))); this.rabbitTemplate.convertAndSend(userExchangeName, "user.statusChanged", (new UserStatusToggleEvent(toggledUser, KeycloakSecurity.getUserId())));
return convert(this.getTenantUsersResource() return toggledUser;
.get(userId).toRepresentation());
} }
@ -512,9 +551,9 @@ public class UserService {
if (!userId.equalsIgnoreCase(KeycloakSecurity.getUserId())) { if (!userId.equalsIgnoreCase(KeycloakSecurity.getUserId())) {
var status = validateExecution(KeycloakSecurity.getUserId(), userId); var status = validateExecution(KeycloakSecurity.getUserId(), userId);
if (status.equals(ValidationStatus.FORBIDDEN)) { if (status.equals(ValidationResult.FORBIDDEN)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "It is not allowed to reset the password of a user with higher ranking roles"); throw new ResponseStatusException(HttpStatus.FORBIDDEN, "It is not allowed to reset the password of a user with higher ranking roles");
} else if (status.equals(ValidationStatus.INVALID)) { } else if (status.equals(ValidationResult.INVALID)) {
throw new NotFoundException("User with id: " + userId + " does not exist"); throw new NotFoundException("User with id: " + userId + " does not exist");
} }
} }
@ -564,22 +603,48 @@ public class UserService {
} }
private enum ValidationStatus { private enum ValidationResult {
ALLOWED, ALLOWED,
FORBIDDEN, FORBIDDEN,
INVALID INVALID,
ROLE_REMOVAL
} }
private ValidationStatus validateExecution(String executingUserId, String targetUserId) { private ValidationResult validateExecution(String executingUserId, String targetUserId) {
var currentUserResource = getUserResource(executingUserId); var currentUserResource = getUserResource(executingUserId);
var currentRoles = getRoles(currentUserResource.toRepresentation().getId()); var currentRoles = getRoles(currentUserResource.toRepresentation().getId());
var userRoles = getRoles(targetUserId); var userRoles = getRoles(targetUserId);
return validateRoleRanks(currentRoles, userRoles);
}
private ValidationResult validateExecutionForDeletion(String executingUserId, String targetUserId) {
var currentUserResource = getUserResource(executingUserId);
var currentRoles = getRoles(currentUserResource.toRepresentation().getId());
var userRoles = getRoles(targetUserId);
ValidationResult validationResult = validateRoleRanks(currentRoles, userRoles);
if (validationResult == ValidationResult.ALLOWED) {
if (userRoles.stream() if (userRoles.stream()
.anyMatch(ApplicationRoles::isKneconRole)) { .anyMatch(ApplicationRoles::isKneconRole) && userRoles.stream()
return ValidationStatus.INVALID; .anyMatch(ApplicationRoles::isNoKneconRole)) {
return ValidationResult.ROLE_REMOVAL;
}
}
return validationResult;
}
private ValidationResult validateRoleRanks(Set<String> currentRoles, Set<String> userRoles) {
if (!userRoles.isEmpty() && userRoles.stream()
.allMatch(ApplicationRoles::isKneconRole)) {
return ValidationResult.INVALID;
} }
var roleMapping = tenantUserManagementProperties.getKcRoleMapping(); var roleMapping = tenantUserManagementProperties.getKcRoleMapping();
@ -593,9 +658,9 @@ public class UserService {
.orElse(-1); .orElse(-1);
if (targetRank <= maxRank) { if (targetRank <= maxRank) {
return ValidationStatus.ALLOWED; return ValidationResult.ALLOWED;
} else { } else {
return ValidationStatus.FORBIDDEN; return ValidationResult.FORBIDDEN;
} }
} }

View File

@ -22,11 +22,11 @@ fforesight:
- name: KNECON_ADMIN - name: KNECON_ADMIN
set-by-default: false set-by-default: false
rank: 1000 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", "fforesight-read-identity-provider-config","fforesight-write-identity-provider-config", "red-unarchive-dossier", "red-use-support-controller" ] 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", "red-use-support-controller", "red-import-files", "red-process-download", "red-read-download-status" ]
- name: KNECON_SUPPORT - name: KNECON_SUPPORT
set-by-default: false set-by-default: false
rank: 1000 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", "fforesight-read-identity-provider-config","fforesight-write-identity-provider-config", "red-unarchive-dossier", "red-use-support-controller" ] 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", "red-use-support-controller", "red-process-download", "red-read-download-status" ]
- name: RED_USER - name: RED_USER
set-by-default: true set-by-default: true
rank: 100 rank: 100

View File

@ -48,11 +48,11 @@ fforesight:
- name: KNECON_ADMIN - name: KNECON_ADMIN
set-by-default: false set-by-default: false
rank: 1000 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", "red-use-support-controller" ] 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", "red-use-support-controller", "red-import-files", "red-process-download", "red-read-download-status" ]
- name: KNECON_SUPPORT - name: KNECON_SUPPORT
set-by-default: false set-by-default: false
rank: 1000 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", "red-use-support-controller" ] 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", "red-use-support-controller", "red-process-download", "red-read-download-status" ]
- name: RED_USER_ADMIN - name: RED_USER_ADMIN
set-by-default: false set-by-default: false
rank: 400 rank: 400

View File

@ -72,4 +72,22 @@ public class SMTPConfigurationTest extends AbstractTenantUserManagementIntegrati
TenantContext.clear(); TenantContext.clear();
} }
@Test
public void testSMTPwithoutPassword() {
TenantContext.setTenantId(AbstractTenantUserManagementIntegrationTest.TEST_TENANT_ID);
SMTPConfiguration smtpConfiguration = new SMTPConfiguration();
smtpConfiguration.setFrom("from@knecon.com");
smtpConfiguration.setHost("test.knecon.com");
smtpConfigurationClient.updateSMTPConfiguration(smtpConfiguration);
var current = smtpConfigurationClient.getCurrentSMTPConfiguration();
assertThat(current.getPassword()).matches("\\**");
TenantContext.clear();
}
} }

View File

@ -18,7 +18,8 @@ import com.knecon.fforesight.tenantcommons.model.SearchConnection;
import com.knecon.fforesight.tenantusermanagement.AbstractTenantUserManagementIntegrationTest; import com.knecon.fforesight.tenantusermanagement.AbstractTenantUserManagementIntegrationTest;
import com.knecon.fforesight.tenantusermanagement.feigntestclients.external.TenantsClient; import com.knecon.fforesight.tenantusermanagement.feigntestclients.external.TenantsClient;
import com.knecon.fforesight.tenantusermanagement.model.SearchConnectionRequest; import com.knecon.fforesight.tenantusermanagement.model.SearchConnectionRequest;
import com.knecon.fforesight.tenantusermanagement.model.TenantRequest; import com.knecon.fforesight.tenantusermanagement.model.CreateTenantRequest;
import com.knecon.fforesight.tenantusermanagement.model.UpdateTenantRequest;
import com.knecon.fforesight.tenantusermanagement.service.RealmService; import com.knecon.fforesight.tenantusermanagement.service.RealmService;
import com.knecon.fforesight.tenantusermanagement.utils.TestTenantService; import com.knecon.fforesight.tenantusermanagement.utils.TestTenantService;
@ -68,8 +69,7 @@ public class TenantsTest extends AbstractTenantUserManagementIntegrationTest {
testTenantService.createTestTenantIfNotExists(tenantId, minioPort); testTenantService.createTestTenantIfNotExists(tenantId, minioPort);
TenantContext.setTenantId(tenantId); TenantContext.setTenantId(tenantId);
var tenantRequest = TenantRequest.builder() var tenantRequest = UpdateTenantRequest.builder()
.tenantId(tenantId)
.displayName("updated_display_name") .displayName("updated_display_name")
.searchConnection(SearchConnectionRequest.builder() .searchConnection(SearchConnectionRequest.builder()
.numberOfReplicas("1") .numberOfReplicas("1")
@ -159,8 +159,7 @@ public class TenantsTest extends AbstractTenantUserManagementIntegrationTest {
testTenantService.createTestTenantIfNotExists("new_tenant", minioPort); testTenantService.createTestTenantIfNotExists("new_tenant", minioPort);
TenantContext.setTenantId("new_tenant"); TenantContext.setTenantId("new_tenant");
var tenantRequest = TenantRequest.builder() var tenantRequest = UpdateTenantRequest.builder()
.tenantId("new_tenant")
.s3StorageConnection(S3StorageConnection.builder() .s3StorageConnection(S3StorageConnection.builder()
.key("updated_key") .key("updated_key")
.bucketName("updated_bucket") .bucketName("updated_bucket")
@ -184,8 +183,7 @@ public class TenantsTest extends AbstractTenantUserManagementIntegrationTest {
testTenantService.createTestTenantWithoutStorageIfNotExist("new_tenant_without_storage"); testTenantService.createTestTenantWithoutStorageIfNotExist("new_tenant_without_storage");
TenantContext.setTenantId("new_tenant_without_storage"); TenantContext.setTenantId("new_tenant_without_storage");
var tenantRequest = TenantRequest.builder() var tenantRequest = UpdateTenantRequest.builder()
.tenantId("new_tenant_without_storage")
.azureStorageConnection(AzureStorageConnection.builder().connectionString("updated_connection").containerName("updated_container").build()) .azureStorageConnection(AzureStorageConnection.builder().connectionString("updated_connection").containerName("updated_container").build())
.build(); .build();
@ -202,8 +200,7 @@ public class TenantsTest extends AbstractTenantUserManagementIntegrationTest {
testTenantService.createTestTenantIfNotExists("new_tenant", minioPort); testTenantService.createTestTenantIfNotExists("new_tenant", minioPort);
TenantContext.setTenantId("new_tenant"); TenantContext.setTenantId("new_tenant");
var tenantRequest = TenantRequest.builder() var tenantRequest = UpdateTenantRequest.builder()
.tenantId("new_tenant")
.azureStorageConnection(AzureStorageConnection.builder().connectionString("updated_connection").containerName("updated_container").build()) .azureStorageConnection(AzureStorageConnection.builder().connectionString("updated_connection").containerName("updated_container").build())
.s3StorageConnection(S3StorageConnection.builder() .s3StorageConnection(S3StorageConnection.builder()
.key("updated_key") .key("updated_key")
@ -228,8 +225,7 @@ public class TenantsTest extends AbstractTenantUserManagementIntegrationTest {
testTenantService.createTestTenantIfNotExists("new_tenant_with_s3", minioPort); testTenantService.createTestTenantIfNotExists("new_tenant_with_s3", minioPort);
TenantContext.setTenantId("new_tenant_with_s3"); TenantContext.setTenantId("new_tenant_with_s3");
var tenantRequest = TenantRequest.builder() var tenantRequest = UpdateTenantRequest.builder()
.tenantId("new_tenant_with_s3")
.azureStorageConnection(AzureStorageConnection.builder().connectionString("updated_connection").containerName("updated_container").build()) .azureStorageConnection(AzureStorageConnection.builder().connectionString("updated_connection").containerName("updated_container").build())
.build(); .build();

View File

@ -9,11 +9,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.representations.idm.RoleRepresentation;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import com.knecon.fforesight.tenantcommons.TenantContext; import com.knecon.fforesight.tenantcommons.TenantContext;
@ -26,6 +28,7 @@ import com.knecon.fforesight.tenantusermanagement.model.UpdateProfileRequest;
import com.knecon.fforesight.tenantusermanagement.model.User; import com.knecon.fforesight.tenantusermanagement.model.User;
import com.knecon.fforesight.tenantusermanagement.permissions.ApplicationRoles; import com.knecon.fforesight.tenantusermanagement.permissions.ApplicationRoles;
import com.knecon.fforesight.tenantusermanagement.properties.TenantUserManagementProperties; import com.knecon.fforesight.tenantusermanagement.properties.TenantUserManagementProperties;
import com.knecon.fforesight.tenantusermanagement.service.RealmService;
import feign.FeignException; import feign.FeignException;
import lombok.SneakyThrows; import lombok.SneakyThrows;
@ -35,6 +38,9 @@ public class UserTest extends AbstractTenantUserManagementIntegrationTest {
@Autowired @Autowired
private UserClient userClient; private UserClient userClient;
@Autowired
private RealmService realmService;
@Autowired @Autowired
private TenantUserManagementProperties tenantUserManagementProperties; private TenantUserManagementProperties tenantUserManagementProperties;
@ -386,24 +392,24 @@ public class UserTest extends AbstractTenantUserManagementIntegrationTest {
createUserRequest.setFirstName("All"); createUserRequest.setFirstName("All");
createUserRequest.setLastName("Roles"); createUserRequest.setLastName("Roles");
createUserRequest.setUsername("AllRoles"); createUserRequest.setUsername("AllRoles");
createUserRequest.setRoles(allRoles);
User user = userClient.createUser(createUserRequest); User user = userClient.createUser(createUserRequest);
addRoles(user.getUserId(), allRoles);
var createUserRequest2 = new CreateUserRequest(); var createUserRequest2 = new CreateUserRequest();
createUserRequest2.setEmail("nokneconroles@notknecon.com"); createUserRequest2.setEmail("nokneconroles@notknecon.com");
createUserRequest2.setFirstName("No Knecon"); createUserRequest2.setFirstName("No Knecon");
createUserRequest2.setLastName("Roles"); createUserRequest2.setLastName("Roles");
createUserRequest2.setUsername("NoKneconRoles"); createUserRequest2.setUsername("NoKneconRoles");
createUserRequest2.setRoles(allButKneconRoles);
User noKneconUser = userClient.createUser(createUserRequest2); User noKneconUser = userClient.createUser(createUserRequest2);
addRoles(noKneconUser.getUserId(), allButKneconRoles);
var createUserRequest3 = new CreateUserRequest(); var createUserRequest3 = new CreateUserRequest();
createUserRequest3.setEmail("onlykneconroles@notknecon.com"); createUserRequest3.setEmail("onlykneconroles@notknecon.com");
createUserRequest3.setFirstName("Only Knecon"); createUserRequest3.setFirstName("Only Knecon");
createUserRequest3.setLastName("Roles"); createUserRequest3.setLastName("Roles");
createUserRequest3.setUsername("OnlyKneconRoles"); createUserRequest3.setUsername("OnlyKneconRoles");
createUserRequest3.setRoles(onlyKneconRoles);
User onlyKneconUser = userClient.createUser(createUserRequest3); User onlyKneconUser = userClient.createUser(createUserRequest3);
addRoles(onlyKneconUser.getUserId(), onlyKneconRoles);
var allUsers = userClient.getAllUsers(true); var allUsers = userClient.getAllUsers(true);
@ -480,9 +486,13 @@ public class UserTest extends AbstractTenantUserManagementIntegrationTest {
tokenService.setUser("red-user-admin@knecon.com", "Secret@secured!23"); tokenService.setUser("red-user-admin@knecon.com", "Secret@secured!23");
// we should not be able to set roles of this user at all as it is not visible to us resulting in a 404 // we should not be able to set roles of this user at all as it is not visible to us resulting in a 404
e = assertThrows(FeignException.class, () -> userClient.setRoles(user.getUserId(), onlyKneconRoles)); e = assertThrows(FeignException.class, () -> userClient.setRoles(onlyKneconUser.getUserId(), allButKneconRoles));
assertEquals(404, e.status()); assertEquals(404, e.status());
// we can not assign a knecon rule as it is not visible to us
e = assertThrows(FeignException.class, () -> userClient.setRoles(user.getUserId(), onlyKneconRoles));
assertEquals(400, e.status());
// we should not be able to assign ourselves a knecon role as it is not visible to us // we should not be able to assign ourselves a knecon role as it is not visible to us
e = assertThrows(FeignException.class, () -> userClient.setRoles(redUserAdmin.getUserId(), allRoles)); e = assertThrows(FeignException.class, () -> userClient.setRoles(redUserAdmin.getUserId(), allRoles));
assertEquals(400, e.status()); assertEquals(400, e.status());
@ -490,59 +500,99 @@ public class UserTest extends AbstractTenantUserManagementIntegrationTest {
// authenticate as knecon admin again // authenticate as knecon admin again
tokenService.setUser("admin@knecon.com", "secret"); tokenService.setUser("admin@knecon.com", "secret");
// this should be possible because we now have knecon roles // this should still not be possible
userClient.setRoles(onlyKneconUser.getUserId(), allRoles); e = assertThrows(FeignException.class, () -> userClient.setRoles(onlyKneconUser.getUserId(), allRoles));
assertEquals(userClient.getUserById(onlyKneconUser.getUserId()).getRoles().size(), 2); assertEquals(404, e.status());
// and this as well // and also not this
userClient.setRoles(user.getUserId(), allRoles); e = assertThrows(FeignException.class, () -> userClient.setRoles(user.getUserId(), allRoles));
assertEquals(400, e.status());
// we can also poll the user // we can also poll the user
userClient.getUserById(user.getUserId()); userClient.getUserById(user.getUserId());
e = assertThrows(FeignException.class, () -> userClient.getUserById(onlyKneconUser.getUserId()));
assertEquals(404, e.status());
// back to having no rights // back to having no rights
tokenService.setUser("red-user-admin@knecon.com", "Secret@secured!23"); tokenService.setUser("red-user-admin@knecon.com", "Secret@secured!23");
// we can not call update profile // we can not call update profile
e = assertThrows(FeignException.class, () -> userClient.updateProfile(user.getUserId(), new UpdateProfileRequest())); e = assertThrows(FeignException.class, () -> userClient.updateProfile(onlyKneconUser.getUserId(), new UpdateProfileRequest()));
assertEquals(404, e.status()); assertEquals(404, e.status());
// or reset password // or reset password
e = assertThrows(FeignException.class, () -> userClient.resetPassword(user.getUserId(), new ResetPasswordRequest())); e = assertThrows(FeignException.class, () -> userClient.resetPassword(onlyKneconUser.getUserId(), new ResetPasswordRequest()));
assertEquals(404, e.status()); assertEquals(404, e.status());
// now as a knecon admin again // now as a knecon admin again
tokenService.setUser("admin@knecon.com", "secret"); tokenService.setUser("admin@knecon.com", "secret");
// we can also not see another knecon account and change their password // we can also not see another knecon account and change their password
e = assertThrows(FeignException.class, () -> userClient.resetPassword(user.getUserId(), new ResetPasswordRequest())); e = assertThrows(FeignException.class, () -> userClient.resetPassword(onlyKneconUser.getUserId(), new ResetPasswordRequest()));
assertEquals(404, e.status()); assertEquals(404, e.status());
// or activate the profile // or activate the profile
e = assertThrows(FeignException.class, () -> userClient.activateProfile(user.getUserId(), true)); e = assertThrows(FeignException.class, () -> userClient.activateProfile(onlyKneconUser.getUserId(), true));
assertEquals(404, e.status()); assertEquals(404, e.status());
// but the user with all roles can be processed
userClient.resetPassword(user.getUserId(), new ResetPasswordRequest("Secret@secured!23", false));
User activated = userClient.activateProfile(user.getUserId(), true);
activated.getRoles()
.stream()
.noneMatch(ApplicationRoles::isKneconRole);
// we create a new user with all roles // we create a new user with all roles
var createUserRequest4 = new CreateUserRequest(); var createUserRequest4 = new CreateUserRequest();
createUserRequest4.setEmail("allroles2@knecon.com"); createUserRequest4.setEmail("allroles2@knecon.com");
createUserRequest4.setFirstName("All"); createUserRequest4.setFirstName("All");
createUserRequest4.setLastName("Roles2"); createUserRequest4.setLastName("Roles2");
createUserRequest4.setUsername("AllRoles2"); createUserRequest4.setUsername("AllRoles2");
createUserRequest4.setRoles(allRoles);
User user4 = userClient.createUser(createUserRequest4); User user4 = userClient.createUser(createUserRequest4);
addRoles(user4.getUserId(), allRoles);
// we attempt to delete it // we attempt to delete it, should not be possible but still return 204
userClient.deleteUser(user4.getUserId()); userClient.deleteUser(onlyKneconUser.getUserId());
// and again using the bulk call // and again using the bulk call
userClient.deleteUsers(List.of(user4.getUserId())); userClient.deleteUsers(List.of(onlyKneconUser.getUserId()));
// no rights again ...
tokenService.setUser("red-user-admin@knecon.com", "Secret@secured!23");
// with this user we expect a 204 as well
userClient.deleteUser(onlyKneconUser.getUserId());
userClient.deleteUsers(List.of(onlyKneconUser.getUserId()));
// check users as knecon admin again
tokenService.setUser("admin@knecon.com", "secret");
// with this user we expect a 204 as well but the user should have removed red roles
userClient.deleteUser(user4.getUserId());
addRoles(onlyKneconUser.getUserId(), allRoles);
allUsers = userClient.getAllUsers(true); allUsers = userClient.getAllUsers(true);
assertTrue(allUsers.stream() assertTrue(allUsers.stream()
.anyMatch(u -> u.getUserId().equals(user4.getUserId()))); .anyMatch(u -> u.getUserId().equals(onlyKneconUser.getUserId())));
// and should not have changed
var user5 = userClient.getUserById(user4.getUserId()); // hence, 404 when trying to get the user now
assertEquals(user4, user5); e = assertThrows(FeignException.class, () -> userClient.getUserById(user4.getUserId()));
assertEquals(404, e.status());
// give the user the old roles back
addRoles(user4.getUserId(), allButKneconRoles);
allUsers = userClient.getAllUsers(true);
var user4AfterShenanigansOpt = allUsers.stream().filter(u -> u.getUserId().equals(user4.getUserId())).findFirst();
assertTrue(user4AfterShenanigansOpt.isPresent());
user4AfterShenanigansOpt.get().setRoles(new HashSet<>());
assertEquals(user4AfterShenanigansOpt.get(), user4);
var stillOnlyKneconUserOpt = allUsers.stream().filter(u -> u.getUserId().equals(onlyKneconUser.getUserId())).findFirst();
assertTrue(stillOnlyKneconUserOpt.isPresent());
stillOnlyKneconUserOpt.get().setRoles(new HashSet<>());
assertEquals(stillOnlyKneconUserOpt.get(), onlyKneconUser);
} }
@ -563,8 +613,8 @@ public class UserTest extends AbstractTenantUserManagementIntegrationTest {
createUserRequest.setFirstName("Some Other"); createUserRequest.setFirstName("Some Other");
createUserRequest.setLastName("User"); createUserRequest.setLastName("User");
createUserRequest.setUsername("SomeOtherUser"); createUserRequest.setUsername("SomeOtherUser");
createUserRequest.setRoles(allRoles);
User user = userClient.createUser(createUserRequest); User user = userClient.createUser(createUserRequest);
addRoles(user.getUserId(), allRoles);
var createUserRequest2 = new CreateUserRequest(); var createUserRequest2 = new CreateUserRequest();
createUserRequest2.setEmail("noroles@notknecon.com"); createUserRequest2.setEmail("noroles@notknecon.com");
@ -587,6 +637,36 @@ public class UserTest extends AbstractTenantUserManagementIntegrationTest {
} }
@Test
public void testOperationsOnUserWithoutRoles() {
// set context and user
TenantContext.setTenantId(AbstractTenantUserManagementIntegrationTest.TEST_TENANT_ID);
tokenService.setUser("admin@knecon.com", "secret");
var createUserRequest = new CreateUserRequest();
createUserRequest.setEmail("noroles@notknecon.com");
createUserRequest.setFirstName("No");
createUserRequest.setLastName("Roles");
createUserRequest.setUsername("NoRolesAtAll");
createUserRequest.setRoles(new HashSet<>());
User noRolesUser = userClient.createUser(createUserRequest);
userClient.resetPassword(noRolesUser.getUserId(), ResetPasswordRequest.builder().password("SuperSecret42!!").build());
userClient.activateProfile(noRolesUser.getUserId(), false);
noRolesUser = userClient.getUserById(noRolesUser.getUserId());
assertFalse(noRolesUser.isActive());
var allUsers = userClient.getAllUsers(true);
var sizeBefore = allUsers.size();
userClient.deleteUser(noRolesUser.getUserId());
allUsers = userClient.getAllUsers(true);
assertThat(allUsers).hasSize(sizeBefore - 1);
}
@Test @Test
public void testCreateUserWithInvalidEmailFormat() { public void testCreateUserWithInvalidEmailFormat() {
@ -623,4 +703,30 @@ public class UserTest extends AbstractTenantUserManagementIntegrationTest {
} }
private UsersResource getTenantUsersResource() {
return realmService.realm(TenantContext.getTenantId()).users();
}
private UserResource getUserResource(String userId) {
return this.getTenantUsersResource()
.get(userId);
}
private RoleRepresentation getRoleRepresentation(String role) {
return realmService.realm(TenantContext.getTenantId()).roles()
.get(role).toRepresentation();
}
private void addRoles(String userId, Set<String> roles) {
getUserResource(userId).roles().realmLevel().add(roles.stream().map(this::getRoleRepresentation).toList());
}
} }

View File

@ -18,7 +18,7 @@ import com.knecon.fforesight.tenantcommons.model.DatabaseConnection;
import com.knecon.fforesight.tenantcommons.model.S3StorageConnection; import com.knecon.fforesight.tenantcommons.model.S3StorageConnection;
import com.knecon.fforesight.tenantusermanagement.api.internal.InternalTenantsResource; import com.knecon.fforesight.tenantusermanagement.api.internal.InternalTenantsResource;
import com.knecon.fforesight.tenantusermanagement.model.SearchConnectionRequest; import com.knecon.fforesight.tenantusermanagement.model.SearchConnectionRequest;
import com.knecon.fforesight.tenantusermanagement.model.TenantRequest; import com.knecon.fforesight.tenantusermanagement.model.CreateTenantRequest;
import com.knecon.fforesight.tenantusermanagement.model.TenantUser; import com.knecon.fforesight.tenantusermanagement.model.TenantUser;
import com.knecon.fforesight.tenantusermanagement.permissions.ApplicationRoles; import com.knecon.fforesight.tenantusermanagement.permissions.ApplicationRoles;
import com.knecon.fforesight.tenantusermanagement.testcontainers.MongoDBTestContainer; import com.knecon.fforesight.tenantusermanagement.testcontainers.MongoDBTestContainer;
@ -53,8 +53,8 @@ public class TestTenantService {
private void createUser(String testTenantId, int actualPort, boolean withStorage) { private void createUser(String testTenantId, int actualPort, boolean withStorage) {
// not found // not found
TenantRequest tenantRequest; CreateTenantRequest tenantRequest;
var tenantRequestBuilder = TenantRequest.builder() var tenantRequestBuilder = CreateTenantRequest.builder()
.tenantId(testTenantId) .tenantId(testTenantId)
.displayName(testTenantId) .displayName(testTenantId)
.guid(UUID.randomUUID().toString()) .guid(UUID.randomUUID().toString())