From f0415a8ac14a374f9d0de514f99b366d8c32940a Mon Sep 17 00:00:00 2001 From: Matthias Schneider Date: Tue, 12 May 2026 16:44:06 +0200 Subject: [PATCH] Add user management (#54) --- .../common.parent.service/pom.xml | 6 + .../platform_management.common.api/pom.xml | 42 ++ .../api/PlatformManagementApiConfig.java | 0 .../platform_management.common/pom.xml | 22 ++ .../pom.xml | 10 +- .../api}/CredentialCreateRequest.kt | 7 +- .../api}/CredentialManagementRestApi.java | 53 ++- .../CredentialManagementRestApiConfig.java | 11 + .../pom.xml | 47 +++ .../CredentialManagementRestController.java | 32 +- .../pom.xml | 23 ++ .../pom.xml | 38 ++ .../user_management/api/UserCreateRequest.kt | 48 +++ .../api}/UserManagementRestApi.java | 14 +- .../api}/UserManagementRestApiConfig.java | 3 +- .../api/UserCreateRequestValidationTest.java | 128 ++++++ .../pom.xml | 59 +++ .../LastAdminDeletionNotAllowedException.kt | 8 + .../impl/UserManagementRestController.java | 55 +++ .../impl}/UserManagementRuntimeException.kt | 5 +- .../user_management/impl/UserManager.java | 18 + .../impl}/UserManagerImpl.java | 160 ++++---- .../impl}/UserNotFoundException.kt | 6 +- .../UserManagementRestControllerTest.java | 98 +++++ .../impl/UserManagerImplTest.java | 218 +++++++++++ .../pom.xml | 23 ++ .../platform_management.features/pom.xml | 22 ++ .../CredentialManagementRestApiConfig.java | 9 - .../platform_management.service.app/pom.xml | 13 + .../service/app/users/UserCreateRequest.kt | 28 -- .../users/UserManagementRestController.java | 35 -- .../service/app/users/UserManager.java | 9 - .../pom.xml | 2 +- .../client/PlatformManagementClient.java | 2 +- .../PlatformManagementCredentialsClient.java | 4 +- .../platform_management.service/pom.xml | 1 - platform_management/pom.xml | 2 + .../resource_management.common/pom.xml | 7 + .../ResourceCredentialsManager.java | 2 +- .../resources/ResourcesRestController.java | 1 - .../pom.xml | 7 + .../pom.xml | 6 + .../FirmwareUpdatesRestController.java | 1 - .../features/importer/ImporterService.java | 2 +- .../resource_management.service.app/pom.xml | 6 + .../platform-management-client.ts | 33 ++ ui/src/components/core/Drawer.vue | 7 + ui/src/pages/AdminUsersPage.vue | 367 ++++++++++++++++++ ui/src/pages/router.js | 6 + 49 files changed, 1494 insertions(+), 212 deletions(-) create mode 100644 platform_management/platform_management.common/platform_management.common.api/pom.xml rename platform_management/{platform_management.service/platform_management.service.api => platform_management.common/platform_management.common.api}/src/main/java/org/eclipse/slm/platform_management/service/api/PlatformManagementApiConfig.java (100%) create mode 100644 platform_management/platform_management.common/pom.xml rename platform_management/{platform_management.service/platform_management.service.api => platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.api}/pom.xml (82%) rename platform_management/{platform_management.service/platform_management.service.api/src/main/java/org/eclipse/slm/platform_management/service/api/credentials => platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.api/src/main/java/org/eclipse/slm/platform_management/features/credentials_management/api}/CredentialCreateRequest.kt (85%) rename platform_management/{platform_management.service/platform_management.service.api/src/main/java/org/eclipse/slm/platform_management/service/api/credentials => platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.api/src/main/java/org/eclipse/slm/platform_management/features/credentials_management/api}/CredentialManagementRestApi.java (67%) create mode 100644 platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.api/src/main/java/org/eclipse/slm/platform_management/features/credentials_management/api/CredentialManagementRestApiConfig.java create mode 100644 platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.impl/pom.xml rename platform_management/{platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/credentials => platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.impl/src/main/java/org/eclipse/slm/platform_management/features/credentials_management/impl}/CredentialManagementRestController.java (83%) create mode 100644 platform_management/platform_management.features/platform_management.features.credentials_management/pom.xml create mode 100644 platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/pom.xml create mode 100644 platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/src/main/java/org/eclipse/slm/platform_management/features/user_management/api/UserCreateRequest.kt rename platform_management/{platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users => platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/src/main/java/org/eclipse/slm/platform_management/features/user_management/api}/UserManagementRestApi.java (60%) rename platform_management/{platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users => platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/src/main/java/org/eclipse/slm/platform_management/features/user_management/api}/UserManagementRestApiConfig.java (68%) create mode 100644 platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/src/test/java/org/eclipse/slm/platform_management/features/user_management/api/UserCreateRequestValidationTest.java create mode 100644 platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/pom.xml create mode 100644 platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/LastAdminDeletionNotAllowedException.kt create mode 100644 platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManagementRestController.java rename platform_management/{platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users => platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl}/UserManagementRuntimeException.kt (70%) create mode 100644 platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManager.java rename platform_management/{platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users => platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl}/UserManagerImpl.java (57%) rename platform_management/{platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users => platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl}/UserNotFoundException.kt (73%) create mode 100644 platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/test/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManagementRestControllerTest.java create mode 100644 platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/test/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManagerImplTest.java create mode 100644 platform_management/platform_management.features/platform_management.features.user_management/pom.xml create mode 100644 platform_management/platform_management.features/pom.xml delete mode 100644 platform_management/platform_management.service/platform_management.service.api/src/main/java/org/eclipse/slm/platform_management/service/api/credentials/CredentialManagementRestApiConfig.java delete mode 100644 platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserCreateRequest.kt delete mode 100644 platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManagementRestController.java delete mode 100644 platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManager.java create mode 100644 ui/src/pages/AdminUsersPage.vue diff --git a/common/common.parent/common.parent.service/pom.xml b/common/common.parent/common.parent.service/pom.xml index 45b0cc8a6..c93012c8d 100644 --- a/common/common.parent/common.parent.service/pom.xml +++ b/common/common.parent/common.parent.service/pom.xml @@ -97,6 +97,12 @@ ${project.version} compile + + org.eclipse.slm + common.utils.keycloak + ${project.version} + compile + diff --git a/platform_management/platform_management.common/platform_management.common.api/pom.xml b/platform_management/platform_management.common/platform_management.common.api/pom.xml new file mode 100644 index 000000000..61128cfbb --- /dev/null +++ b/platform_management/platform_management.common/platform_management.common.api/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + platform_management.common.api + ${revision} + jar + + + org.eclipse.slm + platform_management.common + ${revision} + + + + + + io.swagger.core.v3 + swagger-annotations + + + + org.springframework.boot + spring-boot-starter-web + + + org.eclipse.slm + platform_management.features.credentials_management.api + ${project.version} + compile + + + org.eclipse.slm + platform_management.features.user_management.api + ${project.version} + compile + + + + \ No newline at end of file diff --git a/platform_management/platform_management.service/platform_management.service.api/src/main/java/org/eclipse/slm/platform_management/service/api/PlatformManagementApiConfig.java b/platform_management/platform_management.common/platform_management.common.api/src/main/java/org/eclipse/slm/platform_management/service/api/PlatformManagementApiConfig.java similarity index 100% rename from platform_management/platform_management.service/platform_management.service.api/src/main/java/org/eclipse/slm/platform_management/service/api/PlatformManagementApiConfig.java rename to platform_management/platform_management.common/platform_management.common.api/src/main/java/org/eclipse/slm/platform_management/service/api/PlatformManagementApiConfig.java diff --git a/platform_management/platform_management.common/pom.xml b/platform_management/platform_management.common/pom.xml new file mode 100644 index 000000000..9adea5f83 --- /dev/null +++ b/platform_management/platform_management.common/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + platform_management.common + ${revision} + pom + + + org.eclipse.slm + platform_management + ${revision} + + + + platform_management.common.api + + + + diff --git a/platform_management/platform_management.service/platform_management.service.api/pom.xml b/platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.api/pom.xml similarity index 82% rename from platform_management/platform_management.service/platform_management.service.api/pom.xml rename to platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.api/pom.xml index f95257646..4a90bf8dd 100644 --- a/platform_management/platform_management.service/platform_management.service.api/pom.xml +++ b/platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.api/pom.xml @@ -4,28 +4,25 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - platform_management.service.api + platform_management.features.credentials_management.api ${revision} jar org.eclipse.slm - platform_management.service + platform_management.features.credentials_management ${revision} - io.swagger.core.v3 swagger-annotations - org.springframework.boot spring-boot-starter-web - org.eclipse.slm common.credentials @@ -34,4 +31,5 @@ - \ No newline at end of file + + diff --git a/platform_management/platform_management.service/platform_management.service.api/src/main/java/org/eclipse/slm/platform_management/service/api/credentials/CredentialCreateRequest.kt b/platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.api/src/main/java/org/eclipse/slm/platform_management/features/credentials_management/api/CredentialCreateRequest.kt similarity index 85% rename from platform_management/platform_management.service/platform_management.service.api/src/main/java/org/eclipse/slm/platform_management/service/api/credentials/CredentialCreateRequest.kt rename to platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.api/src/main/java/org/eclipse/slm/platform_management/features/credentials_management/api/CredentialCreateRequest.kt index 2a0acf6e4..ec83a7b9b 100644 --- a/platform_management/platform_management.service/platform_management.service.api/src/main/java/org/eclipse/slm/platform_management/service/api/credentials/CredentialCreateRequest.kt +++ b/platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.api/src/main/java/org/eclipse/slm/platform_management/features/credentials_management/api/CredentialCreateRequest.kt @@ -1,4 +1,4 @@ -package org.eclipse.slm.platform_management.service.api.credentials +package org.eclipse.slm.platform_management.features.credentials_management.api import com.fasterxml.jackson.annotation.JsonProperty import org.eclipse.slm.common.credentials.model.Credential @@ -14,5 +14,6 @@ data class CredentialCreateRequest( @param:JsonProperty("fullPathOwnerGroupId") val fullPathOwnerGroupId: String -) { -} \ No newline at end of file +) + + diff --git a/platform_management/platform_management.service/platform_management.service.api/src/main/java/org/eclipse/slm/platform_management/service/api/credentials/CredentialManagementRestApi.java b/platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.api/src/main/java/org/eclipse/slm/platform_management/features/credentials_management/api/CredentialManagementRestApi.java similarity index 67% rename from platform_management/platform_management.service/platform_management.service.api/src/main/java/org/eclipse/slm/platform_management/service/api/credentials/CredentialManagementRestApi.java rename to platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.api/src/main/java/org/eclipse/slm/platform_management/features/credentials_management/api/CredentialManagementRestApi.java index b8330361f..d08db01e9 100644 --- a/platform_management/platform_management.service/platform_management.service.api/src/main/java/org/eclipse/slm/platform_management/service/api/credentials/CredentialManagementRestApi.java +++ b/platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.api/src/main/java/org/eclipse/slm/platform_management/features/credentials_management/api/CredentialManagementRestApi.java @@ -1,11 +1,16 @@ -package org.eclipse.slm.platform_management.service.api.credentials; +package org.eclipse.slm.platform_management.features.credentials_management.api; import io.swagger.v3.oas.annotations.Operation; import org.eclipse.slm.common.credentials.model.CredentialData; import org.eclipse.slm.common.credentials.model.CredentialEntityLinkCreateDTO; import org.eclipse.slm.common.credentials.model.CredentialReadDTO; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; import java.util.List; import java.util.UUID; @@ -14,51 +19,56 @@ public interface CredentialManagementRestApi { @RequestMapping(value = "/{credentialId}", method = RequestMethod.GET) @Operation(summary = "Get credential by id") - @ResponseBody ResponseEntity getCredentialById( - @PathVariable(name = "credentialId") UUID credentialId); + @ResponseBody + ResponseEntity getCredentialById(@PathVariable(name = "credentialId") UUID credentialId); @RequestMapping(value = "/{credentialId}/data", method = RequestMethod.GET) @Operation(summary = "Get credential data by id") - @ResponseBody ResponseEntity getCredentialDataById( + @ResponseBody + ResponseEntity getCredentialDataById( @PathVariable(name = "credentialId") UUID credentialId, - @RequestParam(name = "impersonatedGroupId") String impersonatedGroupId - ); + @RequestParam(name = "impersonatedGroupId") String impersonatedGroupId); @RequestMapping(value = "/", method = RequestMethod.POST) @Operation(summary = "Create credential") - @ResponseBody ResponseEntity createCredential( - @RequestBody CredentialCreateRequest credentialCreateRequest); + @ResponseBody + ResponseEntity createCredential(@RequestBody CredentialCreateRequest credentialCreateRequest); @RequestMapping(value = "/{credentialId}", method = RequestMethod.PUT) @Operation(summary = "Create or update credential") - @ResponseBody ResponseEntity createOrUpdateCredential( - @PathVariable(name = "credentialId") UUID credentialId, + @ResponseBody + ResponseEntity createOrUpdateCredential( + @PathVariable(name = "credentialId") UUID credentialId, @RequestBody CredentialCreateRequest credentialCreateRequest); @RequestMapping(value = "/{credentialId}", method = RequestMethod.DELETE) @Operation(summary = "Delete credential by id") - @ResponseBody ResponseEntity deleteCredential( - @PathVariable(name = "credentialId") UUID credentialId); + @ResponseBody + ResponseEntity deleteCredential(@PathVariable(name = "credentialId") UUID credentialId); @RequestMapping(value = "/entities/{entityId}", method = RequestMethod.GET) @Operation(summary = "Get credentials of entity") - @ResponseBody ResponseEntity> getCredentialsOfEntity( + @ResponseBody + ResponseEntity> getCredentialsOfEntity( @PathVariable(name = "entityId") String entityId, @RequestParam(value = "entityType") String entityType); @RequestMapping(value = "", method = RequestMethod.GET) @Operation(summary = "Get credentials of user") - @ResponseBody ResponseEntity> getCredentialsOfUser(); + @ResponseBody + ResponseEntity> getCredentialsOfUser(); @RequestMapping(value = "/{credentialId}/links", method = RequestMethod.POST) @Operation(summary = "Link existing credential to entity") - @ResponseBody ResponseEntity linkCredentialToEntity( + @ResponseBody + ResponseEntity linkCredentialToEntity( @PathVariable(name = "credentialId") UUID credentialId, @RequestBody CredentialEntityLinkCreateDTO entityLink); @RequestMapping(value = "/{credentialId}/links", method = RequestMethod.DELETE) @Operation(summary = "Delete credential entity link") - @ResponseBody ResponseEntity deleteCredentialEntityLink( + @ResponseBody + ResponseEntity deleteCredentialEntityLink( @PathVariable(name = "credentialId") UUID credentialId, @RequestParam(value = "entityType") String entityType, @RequestParam(value = "entityId") String entityId, @@ -66,14 +76,17 @@ public interface CredentialManagementRestApi { @RequestMapping(value = "/{credentialId}/scopes", method = RequestMethod.POST) @Operation(summary = "Add scopes to credential") - @ResponseBody ResponseEntity addCredentialScopes( + @ResponseBody + ResponseEntity addCredentialScopes( @PathVariable(name = "credentialId") UUID credentialId, @RequestBody List scopes); @RequestMapping(value = "/{credentialId}/scopes", method = RequestMethod.DELETE) @Operation(summary = "Remove scopes from credential") - @ResponseBody ResponseEntity removeCredentialScopes( + @ResponseBody + ResponseEntity removeCredentialScopes( @PathVariable(name = "credentialId") UUID credentialId, @RequestBody List scopes); - } + + diff --git a/platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.api/src/main/java/org/eclipse/slm/platform_management/features/credentials_management/api/CredentialManagementRestApiConfig.java b/platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.api/src/main/java/org/eclipse/slm/platform_management/features/credentials_management/api/CredentialManagementRestApiConfig.java new file mode 100644 index 000000000..d1f3c7513 --- /dev/null +++ b/platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.api/src/main/java/org/eclipse/slm/platform_management/features/credentials_management/api/CredentialManagementRestApiConfig.java @@ -0,0 +1,11 @@ +package org.eclipse.slm.platform_management.features.credentials_management.api; + +public class CredentialManagementRestApiConfig { + + public static final String BASE_PATH = "/credentials"; + + public static final String TAG = "Credential Management"; + +} + + diff --git a/platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.impl/pom.xml b/platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.impl/pom.xml new file mode 100644 index 000000000..ac78d8435 --- /dev/null +++ b/platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.impl/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + platform_management.features.credentials_management.impl + ${revision} + jar + + + org.eclipse.slm + platform_management.features.credentials_management + ${revision} + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.security + spring-security-oauth2-resource-server + + + org.eclipse.slm + common.credentials + ${project.version} + compile + + + org.eclipse.slm + common.utils.keycloak + ${project.version} + compile + + + org.eclipse.slm + platform_management.features.credentials_management.api + ${project.version} + compile + + + + + diff --git a/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/credentials/CredentialManagementRestController.java b/platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.impl/src/main/java/org/eclipse/slm/platform_management/features/credentials_management/impl/CredentialManagementRestController.java similarity index 83% rename from platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/credentials/CredentialManagementRestController.java rename to platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.impl/src/main/java/org/eclipse/slm/platform_management/features/credentials_management/impl/CredentialManagementRestController.java index 80bb55678..c74deecec 100644 --- a/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/credentials/CredentialManagementRestController.java +++ b/platform_management/platform_management.features/platform_management.features.credentials_management/platform_management.features.credentials_management.impl/src/main/java/org/eclipse/slm/platform_management/features/credentials_management/impl/CredentialManagementRestController.java @@ -1,21 +1,20 @@ -package org.eclipse.slm.platform_management.service.app.credentials; +package org.eclipse.slm.platform_management.features.credentials_management.impl; import io.swagger.v3.oas.annotations.tags.Tag; import org.eclipse.slm.common.credentials.CredentialsManager; import org.eclipse.slm.common.credentials.model.CredentialData; +import org.eclipse.slm.common.credentials.model.CredentialEntityLinkCreateDTO; import org.eclipse.slm.common.credentials.model.CredentialReadDTO; -import org.eclipse.slm.common.restserver.annotations.AuthorizedAsSlmUser; import org.eclipse.slm.common.utils.keycloak.KeycloakTokenUtil; -import org.eclipse.slm.platform_management.service.api.credentials.CredentialCreateRequest; -import org.eclipse.slm.platform_management.service.api.credentials.CredentialManagementRestApi; -import org.eclipse.slm.platform_management.service.api.credentials.CredentialManagementRestApiConfig; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.eclipse.slm.platform_management.features.credentials_management.api.CredentialCreateRequest; +import org.eclipse.slm.platform_management.features.credentials_management.api.CredentialManagementRestApi; +import org.eclipse.slm.platform_management.features.credentials_management.api.CredentialManagementRestApiConfig; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.UUID; @@ -25,8 +24,6 @@ @Tag(name = CredentialManagementRestApiConfig.TAG) public class CredentialManagementRestController implements CredentialManagementRestApi { - private final static Logger LOG = LoggerFactory.getLogger(CredentialManagementRestController.class); - private final CredentialsManager credentialsManager; public CredentialManagementRestController(CredentialsManager credentialsManager) { @@ -44,14 +41,12 @@ public ResponseEntity getCredentialById(UUID credentialId) { @Override @PreAuthorize("authentication.tokenAttributes['client_id'] == 'resource_management'") public ResponseEntity getCredentialDataById(UUID credentialId, String impersonatedGroupId) { - var jwtAuthenticationToken = (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); var credentialData = this.credentialsManager.getCredentialDataById(credentialId, impersonatedGroupId); return ResponseEntity.ok(credentialData); } @Override public ResponseEntity createCredential(CredentialCreateRequest credentialCreateRequest) { - var jwtAuthenticationToken = (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); credentialCreateRequest.getCredential().setId(UUID.randomUUID()); this.credentialsManager.createCredential( credentialCreateRequest.getCredential(), @@ -62,9 +57,9 @@ public ResponseEntity createCredential(CredentialCreateRequest credentialC @Override public ResponseEntity createOrUpdateCredential(UUID credentialId, CredentialCreateRequest credentialCreateRequest) { - var jwtAuthenticationToken = (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); credentialCreateRequest.getCredential().setId(credentialId); - this.credentialsManager.createCredential(credentialCreateRequest.getCredential(), + this.credentialsManager.createCredential( + credentialCreateRequest.getCredential(), credentialCreateRequest.getEntityLinks(), credentialCreateRequest.getFullPathOwnerGroupId()); return ResponseEntity.ok().build(); @@ -74,9 +69,7 @@ public ResponseEntity createOrUpdateCredential(UUID credentialId, Credenti public ResponseEntity deleteCredential(UUID credentialId) { var jwtAuthenticationToken = (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); var userAccessToken = KeycloakTokenUtil.getToken(jwtAuthenticationToken); - this.credentialsManager.deleteCredentialForUser(credentialId, userAccessToken); - return ResponseEntity.ok().build(); } @@ -97,14 +90,13 @@ public ResponseEntity> getCredentialsOfUser() { var userGroups = (List) groupsClaimList; var credentials = this.credentialsManager.getAllCredentialsForUser(userGroups, userAccessToken); return ResponseEntity.ok(credentials); - } else { - throw new IllegalStateException("User groups claim is missing or invalid in the JWT token"); } + throw new IllegalStateException("User groups claim is missing or invalid in the JWT token"); } @Override - public ResponseEntity linkCredentialToEntity(UUID credentialId, org.eclipse.slm.common.credentials.model.CredentialEntityLinkCreateDTO entityLink) { + public ResponseEntity linkCredentialToEntity(UUID credentialId, CredentialEntityLinkCreateDTO entityLink) { var jwtAuthenticationToken = (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); var userAccessToken = KeycloakTokenUtil.getToken(jwtAuthenticationToken); this.credentialsManager.getCredentialByIdForUser(credentialId, userAccessToken); @@ -139,3 +131,5 @@ public ResponseEntity removeCredentialScopes(UUID credentialId, List + + 4.0.0 + + platform_management.features.credentials_management + ${revision} + pom + + + org.eclipse.slm + platform_management.features + ${revision} + + + + platform_management.features.credentials_management.api + platform_management.features.credentials_management.impl + + + + diff --git a/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/pom.xml b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/pom.xml new file mode 100644 index 000000000..17e4a5bf7 --- /dev/null +++ b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + platform_management.features.user_management.api + ${revision} + jar + + + org.eclipse.slm + platform_management.features.user_management + ${revision} + + + + + io.swagger.core.v3 + swagger-annotations + + + org.springframework.boot + spring-boot-starter-web + + + jakarta.validation + jakarta.validation-api + + + org.springframework.boot + spring-boot-starter-validation + test + + + + + diff --git a/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/src/main/java/org/eclipse/slm/platform_management/features/user_management/api/UserCreateRequest.kt b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/src/main/java/org/eclipse/slm/platform_management/features/user_management/api/UserCreateRequest.kt new file mode 100644 index 000000000..cd28c4280 --- /dev/null +++ b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/src/main/java/org/eclipse/slm/platform_management/features/user_management/api/UserCreateRequest.kt @@ -0,0 +1,48 @@ +package org.eclipse.slm.platform_management.features.user_management.api + +import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Pattern + +data class UserCreateRequest( + + @field:NotBlank(message = "Username must not be blank") + @field:Pattern( + regexp = "^[A-Za-z][A-Za-z0-9_-]*$", + message = "Username must start with a letter and contain only letters, numbers, '-' and '_'", + ) + @param:JsonProperty("username") + val username: String, + + @field:NotBlank(message = "First name must not be blank") + @field:Pattern( + regexp = "^[A-Za-z][A-Za-z .-]*$", + message = "First name must start with a letter and may only contain letters, spaces, '.' and '-'", + ) + @param:JsonProperty("firstName") + val firstName: String, + + @field:NotBlank(message = "Last name must not be blank") + @field:Pattern( + regexp = "^[A-Za-z][A-Za-z.-]*$", + message = "Last name must start with a letter and may only contain letters, '.' and '-'", + ) + @param:JsonProperty("lastName") + val lastName: String, + + @param:JsonProperty("password") + val password: String, + + @param:JsonProperty("isPasswordTemporary") + val isPasswordTemporary: Boolean = false, + + @field:NotBlank(message = "Email must not be blank") + @field:Email(message = "Email must be a valid email address") + @param:JsonProperty("email") + val email: String, + + @param:JsonProperty("isAdmin") + val isAdmin: Boolean = false, +) + diff --git a/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManagementRestApi.java b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/src/main/java/org/eclipse/slm/platform_management/features/user_management/api/UserManagementRestApi.java similarity index 60% rename from platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManagementRestApi.java rename to platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/src/main/java/org/eclipse/slm/platform_management/features/user_management/api/UserManagementRestApi.java index 60f8623a6..76c61b574 100644 --- a/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManagementRestApi.java +++ b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/src/main/java/org/eclipse/slm/platform_management/features/user_management/api/UserManagementRestApi.java @@ -1,4 +1,4 @@ -package org.eclipse.slm.platform_management.service.app.users; +package org.eclipse.slm.platform_management.features.user_management.api; import io.swagger.v3.oas.annotations.Operation; import org.springframework.http.MediaType; @@ -8,13 +8,25 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import java.util.List; +import java.util.Map; + public interface UserManagementRestApi { + @RequestMapping(value = "", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Get users") + ResponseEntity>> getUsers(); + @RequestMapping(value = "", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Add user") ResponseEntity createUser(@RequestBody UserCreateRequest userCreateRequest); + @RequestMapping(value = "{username}/admin", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Grant admin role to user by username") + ResponseEntity makeUserAdmin(@PathVariable(name = "username") String username); + @RequestMapping(value = "{username}", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Delete user by username") ResponseEntity deleteUser(@PathVariable(name = "username") String username); } + diff --git a/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManagementRestApiConfig.java b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/src/main/java/org/eclipse/slm/platform_management/features/user_management/api/UserManagementRestApiConfig.java similarity index 68% rename from platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManagementRestApiConfig.java rename to platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/src/main/java/org/eclipse/slm/platform_management/features/user_management/api/UserManagementRestApiConfig.java index 7a59f410b..8405781d6 100644 --- a/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManagementRestApiConfig.java +++ b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/src/main/java/org/eclipse/slm/platform_management/features/user_management/api/UserManagementRestApiConfig.java @@ -1,10 +1,9 @@ -package org.eclipse.slm.platform_management.service.app.users; +package org.eclipse.slm.platform_management.features.user_management.api; public class UserManagementRestApiConfig { public static final String BASE_PATH = "/users"; public static final String TAG = "User Management"; - } diff --git a/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/src/test/java/org/eclipse/slm/platform_management/features/user_management/api/UserCreateRequestValidationTest.java b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/src/test/java/org/eclipse/slm/platform_management/features/user_management/api/UserCreateRequestValidationTest.java new file mode 100644 index 000000000..b4e4c9b44 --- /dev/null +++ b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.api/src/test/java/org/eclipse/slm/platform_management/features/user_management/api/UserCreateRequestValidationTest.java @@ -0,0 +1,128 @@ +package org.eclipse.slm.platform_management.features.user_management.api; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class UserCreateRequestValidationTest { + + private static ValidatorFactory validatorFactory; + private static Validator validator; + + @BeforeAll + static void initValidator() { + validatorFactory = Validation.buildDefaultValidatorFactory(); + validator = validatorFactory.getValidator(); + } + + @AfterAll + static void closeValidator() { + if (validatorFactory != null) { + validatorFactory.close(); + } + } + + private static UserCreateRequest validRequest() { + return new UserCreateRequest( + "Alice_Admin", + "Alice", + "Doe", + "secret", + false, + "alice@example.org", + false); + } + + @Nested + class UsernameValidationTests { + + @Test + void acceptsUsername_whenItStartsWithLetterAndContainsAllowedCharacters() { + var request = new UserCreateRequest("A1_user-name", "Alice", "Doe", "secret", false, "alice@example.org", false); + + var violations = validator.validate(request); + + assertTrue(violations.stream().noneMatch(v -> "username".equals(v.getPropertyPath().toString()))); + } + + @Test + void rejectsUsername_whenItDoesNotStartWithLetter() { + var request = new UserCreateRequest("1alice", "Alice", "Doe", "secret", false, "alice@example.org", false); + + var violations = validator.validate(request); + + assertTrue(violations.stream().anyMatch(v -> "username".equals(v.getPropertyPath().toString()))); + } + } + + @Nested + class FirstNameValidationTests { + + @Test + void rejectsFirstName_whenItDoesNotStartWithLetter() { + var request = new UserCreateRequest("Alice", "-Alice", "Doe", "secret", false, "alice@example.org", false); + + var violations = validator.validate(request); + + assertTrue(violations.stream().anyMatch(v -> "firstName".equals(v.getPropertyPath().toString()))); + } + + @Test + void acceptsFirstName_whenItStartsWithLetterAndUsesAllowedCharacters() { + var request = new UserCreateRequest("Alice", "A li-ce.", "Doe", "secret", false, "alice@example.org", false); + + var violations = validator.validate(request); + + assertTrue(violations.stream().noneMatch(v -> "firstName".equals(v.getPropertyPath().toString()))); + } + } + + @Nested + class LastNameValidationTests { + + @Test + void rejectsLastName_whenItDoesNotStartWithLetter() { + var request = new UserCreateRequest("Alice", "Alice", ".Doe", "secret", false, "alice@example.org", false); + + var violations = validator.validate(request); + + assertTrue(violations.stream().anyMatch(v -> "lastName".equals(v.getPropertyPath().toString()))); + } + + @Test + void acceptsLastName_whenItStartsWithLetterAndUsesAllowedCharacters() { + var request = new UserCreateRequest("Alice", "Alice", "D-oe.", "secret", false, "alice@example.org", false); + + var violations = validator.validate(request); + + assertTrue(violations.stream().noneMatch(v -> "lastName".equals(v.getPropertyPath().toString()))); + } + } + + @Nested + class EmailValidationTests { + + @Test + void rejectsEmail_whenInvalid() { + var request = new UserCreateRequest("Alice", "Alice", "Doe", "secret", false, "not-an-email", false); + + var violations = validator.validate(request); + + assertTrue(violations.stream().anyMatch(v -> "email".equals(v.getPropertyPath().toString()))); + } + + @Test + void acceptsEmail_whenValid() { + var violations = validator.validate(validRequest()); + + assertTrue(violations.stream().noneMatch(v -> "email".equals(v.getPropertyPath().toString()))); + } + } +} + diff --git a/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/pom.xml b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/pom.xml new file mode 100644 index 000000000..85885b0eb --- /dev/null +++ b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + platform_management.features.user_management.impl + ${revision} + jar + + + org.eclipse.slm + platform_management.features.user_management + ${revision} + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.eclipse.slm + common.consul.client + ${project.version} + compile + + + org.eclipse.slm + common.keycloak.config + ${project.version} + compile + + + org.eclipse.slm + common.vault.client + ${project.version} + compile + + + org.eclipse.slm + common.restserver + ${project.version} + compile + + + org.eclipse.slm + platform_management.features.user_management.api + ${project.version} + compile + + + + + diff --git a/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/LastAdminDeletionNotAllowedException.kt b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/LastAdminDeletionNotAllowedException.kt new file mode 100644 index 000000000..0433cce0b --- /dev/null +++ b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/LastAdminDeletionNotAllowedException.kt @@ -0,0 +1,8 @@ +package org.eclipse.slm.platform_management.features.user_management.impl + +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ResponseStatus + +@ResponseStatus(HttpStatus.CONFLICT) +class LastAdminDeletionNotAllowedException(message: String) : RuntimeException(message) + diff --git a/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManagementRestController.java b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManagementRestController.java new file mode 100644 index 000000000..7572922d5 --- /dev/null +++ b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManagementRestController.java @@ -0,0 +1,55 @@ +package org.eclipse.slm.platform_management.features.user_management.impl; + +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.eclipse.slm.common.restserver.annotations.AuthorizedAsSlmAdminOrApiKey; +import org.eclipse.slm.platform_management.features.user_management.api.UserCreateRequest; +import org.eclipse.slm.platform_management.features.user_management.api.UserManagementRestApi; +import org.eclipse.slm.platform_management.features.user_management.api.UserManagementRestApiConfig; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping(UserManagementRestApiConfig.BASE_PATH) +@Tag(name = UserManagementRestApiConfig.TAG) +public class UserManagementRestController implements UserManagementRestApi { + + private final UserManager userManager; + + public UserManagementRestController(UserManager userManager) { + this.userManager = userManager; + } + + @Override + @AuthorizedAsSlmAdminOrApiKey + public ResponseEntity>> getUsers() { + return ResponseEntity.ok(this.userManager.getUsers()); + } + + @Override + @AuthorizedAsSlmAdminOrApiKey + public ResponseEntity createUser(@Valid @RequestBody UserCreateRequest userCreateRequest) { + this.userManager.createUser(userCreateRequest); + return ResponseEntity.ok().build(); + } + + @Override + @AuthorizedAsSlmAdminOrApiKey + public ResponseEntity makeUserAdmin(String username) { + this.userManager.makeUserAdmin(username); + return ResponseEntity.ok().build(); + } + + @Override + @AuthorizedAsSlmAdminOrApiKey + public ResponseEntity deleteUser(String username) { + this.userManager.deleteUser(username); + return ResponseEntity.ok().build(); + } +} + diff --git a/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManagementRuntimeException.kt b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManagementRuntimeException.kt similarity index 70% rename from platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManagementRuntimeException.kt rename to platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManagementRuntimeException.kt index 12c928820..44fcc2766 100644 --- a/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManagementRuntimeException.kt +++ b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManagementRuntimeException.kt @@ -1,6 +1,7 @@ -package org.eclipse.slm.platform_management.service.app.users +package org.eclipse.slm.platform_management.features.user_management.impl class UserManagementRuntimeException : RuntimeException { constructor(message: String) : super(message) constructor(message: String, cause: Throwable) : super(message, cause) -} \ No newline at end of file +} + diff --git a/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManager.java b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManager.java new file mode 100644 index 000000000..8a1d8fc27 --- /dev/null +++ b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManager.java @@ -0,0 +1,18 @@ +package org.eclipse.slm.platform_management.features.user_management.impl; + +import org.eclipse.slm.platform_management.features.user_management.api.UserCreateRequest; + +import java.util.List; +import java.util.Map; + +public interface UserManager { + + void createUser(UserCreateRequest userCreateRequest); + + List> getUsers(); + + void makeUserAdmin(String username); + + void deleteUser(String username); +} + diff --git a/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManagerImpl.java b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManagerImpl.java similarity index 57% rename from platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManagerImpl.java rename to platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManagerImpl.java index 6941f253a..74fccce19 100644 --- a/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManagerImpl.java +++ b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManagerImpl.java @@ -1,4 +1,4 @@ -package org.eclipse.slm.platform_management.service.app.users; +package org.eclipse.slm.platform_management.features.user_management.impl; import org.eclipse.slm.common.consul.client.ConsulClient; import org.eclipse.slm.common.consul.client.ConsulClientFactory; @@ -16,34 +16,37 @@ import org.eclipse.slm.common.vault.client.exceptions.VaultGroupNotFoundException; import org.eclipse.slm.common.vault.client.exceptions.VaultRuntimeException; import org.eclipse.slm.common.vault.model.acl.GroupType; +import org.eclipse.slm.platform_management.features.user_management.api.UserCreateRequest; import org.keycloak.representations.idm.UserRepresentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; @Component public class UserManagerImpl implements UserManager { - private final static Logger LOG = LoggerFactory.getLogger(UserManagerImpl.class); + private static final Logger LOG = LoggerFactory.getLogger(UserManagerImpl.class); - public final static String KEYCLOAK_PARENT_GROUP_USERS = "users"; - public final static String KEYCLOAK_REALM_ROLE_NAME_SLM_USER = "slm-user"; - public final static String KEYCLOAK_REALM_ROLE_NAME_SLM_ADMIN = "slm-admin"; - public final static String CONSUL_KEYCLOAK_AUTH_METHOD_NAME = "keycloak"; + public static final String KEYCLOAK_PARENT_GROUP_USERS = "users"; + public static final String KEYCLOAK_REALM_ROLE_NAME_SLM_USER = "slm-user"; + public static final String KEYCLOAK_REALM_ROLE_NAME_SLM_ADMIN = "slm-admin"; + public static final String CONSUL_KEYCLOAK_AUTH_METHOD_NAME = "keycloak"; private final MultiTenantKeycloakRegistration multiTenantKeycloakRegistration; - private final ConsulClient consulAdminClient; - private final VaultClient vaultAdminClient; - private final KeycloakAdminClient keycloakAdminClient; - public UserManagerImpl(MultiTenantKeycloakRegistration multiTenantKeycloakRegistration, ConsulClientFactory consulClientFactory, VaultClientFactory vaultClientFactory, KeycloakAdminClient keycloakAdminClient) { + public UserManagerImpl(MultiTenantKeycloakRegistration multiTenantKeycloakRegistration, + ConsulClientFactory consulClientFactory, + VaultClientFactory vaultClientFactory, + KeycloakAdminClient keycloakAdminClient) { this.multiTenantKeycloakRegistration = multiTenantKeycloakRegistration; this.consulAdminClient = consulClientFactory.createAdminClient(); this.vaultAdminClient = vaultClientFactory.createAdminClient(); @@ -56,7 +59,6 @@ public void createUser(UserCreateRequest userCreateRequest) { var fullPathUserGroupId = this.configureKeycloakForUser(userCreateRequest); this.configureConsulForUserGroup(fullPathUserGroupId); this.configureVaultForUserGroup(fullPathUserGroupId); - LOG.info("Successfully created user '{}'", userCreateRequest.getUsername()); } catch (Exception e) { LOG.error("Error creating user '{}', trying to cleanup and delete user", userCreateRequest.getUsername(), e); @@ -68,25 +70,26 @@ public void createUser(UserCreateRequest userCreateRequest) { @Override public void deleteUser(String username) { try { - // Get Keycloak user details var realm = this.multiTenantKeycloakRegistration.getDefaultRealm(); var keycloakUserId = this.keycloakAdminClient.getUserId(username); - var fullPathUserGroupId = "/" + UserManagerImpl.KEYCLOAK_PARENT_GROUP_USERS + "/" + keycloakUserId; - // Consul delete role and binding rule + this.validateAdminDeletion(keycloakUserId, username); + var fullPathUserGroupId = "/" + KEYCLOAK_PARENT_GROUP_USERS + "/" + keycloakUserId; + try { var userGroupRole = this.consulAdminClient.acl().getRoleByName(fullPathUserGroupId); this.consulAdminClient.acl().deleteRoleById(userGroupRole.getId()); - } catch (ConsulRoleNotFoundException e) { - // Role not found, possibly already deleted --> Ignore and continue + } catch (ConsulRoleNotFoundException ignored) { + // Ignore missing Consul role during cleanup. } + var bindingRules = this.consulAdminClient.acl().getBindingRules(); - var userGroupBindingRules = bindingRules.stream().filter(br -> br.getBindName() - .equals(this.consulAdminClient.acl().cleanUserGroupBindingRuleName(fullPathUserGroupId))).toList(); + var userGroupBindingRules = bindingRules.stream() + .filter(br -> br.getBindName().equals(this.consulAdminClient.acl().cleanUserGroupBindingRuleName(fullPathUserGroupId))) + .toList(); for (var bindingRule : userGroupBindingRules) { this.consulAdminClient.acl().deleteBindingRuleById(bindingRule.getId()); } - // Vault delete group and group alias try { var vaultUserGroup = this.vaultAdminClient.acl().getGroupByName(fullPathUserGroupId); if (vaultUserGroup.getAlias() == null) { @@ -94,30 +97,40 @@ public void deleteUser(String username) { } this.vaultAdminClient.auth().removeJwtGroupAlias(vaultUserGroup.getAlias().getId()); this.vaultAdminClient.acl().deleteGroupByName(fullPathUserGroupId); - } catch (VaultGroupNotFoundException e) { - // Group not found, possibly already deleted --> Ignore and continue + } catch (VaultGroupNotFoundException ignored) { + // Ignore missing Vault group during cleanup. } - // Keycloak delete user group and user - this.keycloakAdminClient.deleteChildGroup(realm, keycloakUserId, UserManagerImpl.KEYCLOAK_PARENT_GROUP_USERS); - this.keycloakAdminClient.deleteUser(realm, keycloakUserId); + this.keycloakAdminClient.deleteChildGroup(realm, keycloakUserId, KEYCLOAK_PARENT_GROUP_USERS); + this.keycloakAdminClient.deleteUser(realm, keycloakUserId); LOG.info("Successfully deleted user '{}'", username); } catch (Exception e) { if (e instanceof KeycloakUserNotFoundException) { throw new UserNotFoundException("User '" + username + "' not found", e); } + if (e instanceof LastAdminDeletionNotAllowedException) { + throw (LastAdminDeletionNotAllowedException) e; + } throw new UserManagementRuntimeException("Error deleting user '" + username + "'", e); } } - /** Configure Keycloak for the new user - * - * @param userCreateRequest The user create request - * @return The full path of the created Keycloak user group - */ + private void validateAdminDeletion(String keycloakUserId, String username) { + var adminUserIds = this.keycloakAdminClient.getUserIdsAssignedToRole(KEYCLOAK_REALM_ROLE_NAME_SLM_ADMIN); + var isAdminUser = adminUserIds.contains(keycloakUserId); + + if (!isAdminUser) { + return; + } + + if (adminUserIds.size() <= 1) { + throw new LastAdminDeletionNotAllowedException( + "Cannot delete user '" + username + "' because at least one admin user must remain"); + } + } + private String configureKeycloakForUser(UserCreateRequest userCreateRequest) throws KeycloakGroupRuntimeException, KeycloakGroupNotFoundException, KeycloakUserNotFoundException, KeycloakUserRuntimeException { - // Create Keycloak user including users group var realm = this.multiTenantKeycloakRegistration.getDefaultRealm(); var keycloakUserRepresentation = new UserRepresentation(); keycloakUserRepresentation.setUsername(userCreateRequest.getUsername()); @@ -127,79 +140,94 @@ private String configureKeycloakForUser(UserCreateRequest userCreateRequest) keycloakUserRepresentation.setLastName(userCreateRequest.getLastName()); var createdKeycloakUser = this.keycloakAdminClient.createUser(realm, keycloakUserRepresentation); var keycloakUserId = UUID.fromString(createdKeycloakUser.getId()); - var fullPathUserGroupId = "/" + UserManagerImpl.KEYCLOAK_PARENT_GROUP_USERS + "/" + keycloakUserId; + var fullPathUserGroupId = "/" + KEYCLOAK_PARENT_GROUP_USERS + "/" + keycloakUserId; this.keycloakAdminClient.setUserPassword(keycloakUserId.toString(), userCreateRequest.getPassword(), userCreateRequest.isPasswordTemporary()); - - this.keycloakAdminClient.assignUserToRealmRole(UserManagerImpl.KEYCLOAK_REALM_ROLE_NAME_SLM_USER, keycloakUserId.toString()); + this.keycloakAdminClient.assignUserToRealmRole(KEYCLOAK_REALM_ROLE_NAME_SLM_USER, keycloakUserId.toString()); if (userCreateRequest.isAdmin()) { - this.keycloakAdminClient.assignUserToRealmRole(UserManagerImpl.KEYCLOAK_REALM_ROLE_NAME_SLM_ADMIN, keycloakUserId.toString()); + this.keycloakAdminClient.assignUserToRealmRole(KEYCLOAK_REALM_ROLE_NAME_SLM_ADMIN, keycloakUserId.toString()); } - this.keycloakAdminClient.createChildGroup(realm, keycloakUserId.toString(), + this.keycloakAdminClient.createChildGroup( + realm, + keycloakUserId.toString(), "Group of user '" + userCreateRequest.getUsername() + "'", - UserManagerImpl.KEYCLOAK_PARENT_GROUP_USERS, + KEYCLOAK_PARENT_GROUP_USERS, Map.of()); this.keycloakAdminClient.assignUserToGroup(realm, keycloakUserId.toString(), keycloakUserId); return fullPathUserGroupId; } - /** Configure Consul for the Keycloak user group - * - * @param fullPathUserGroupId The full path of the Keycloak user group - */ private void configureConsulForUserGroup(String fullPathUserGroupId) { - // Create Consul role user group try { this.consulAdminClient.acl().createRole(fullPathUserGroupId, "Role for Keycloak user group '" + fullPathUserGroupId + "'", List.of()); } catch (ConsulRuntimeException e) { - if (e.getCause().getMessage().contains("already exists")) { - // Role already exists, ignore - } else { + if (!e.getCause().getMessage().contains("already exists")) { throw e; } } - // Create Consul binding rule matching the Keycloak user group + try { var bindingRuleUserGroup = new BindingRule( null, "Binding rule of group '" + fullPathUserGroupId + "' to auth method 'keycloak'", - UserManagerImpl.CONSUL_KEYCLOAK_AUTH_METHOD_NAME, + CONSUL_KEYCLOAK_AUTH_METHOD_NAME, "\"" + fullPathUserGroupId + "\" in list.groups", "role", - fullPathUserGroupId - ); + fullPathUserGroupId); this.consulAdminClient.acl().createBindingRule(bindingRuleUserGroup); } catch (ConsulRuntimeException e) { - if (e.getCause().getMessage().contains("already exists")) { - // Binding rule already exists, ignore - } else { + if (!e.getCause().getMessage().contains("already exists")) { throw e; } } } - /** Configure Vault for the Keycloak user group - * - * @param fullPathUserGroupId The full path of the Keycloak user group - */ private void configureVaultForUserGroup(String fullPathUserGroupId) { - // Create Vault group for matching Keycloak users group - vaultAdminClient.acl().createOrUpdateGroup(fullPathUserGroupId, GroupType.EXTERNAL, List.of()); - var vaultUserGroup = vaultAdminClient.acl().getGroupByName(fullPathUserGroupId); - var mountAccessor = vaultAdminClient.auth().getJwtAuthMethod().getAccessor(); + this.vaultAdminClient.acl().createOrUpdateGroup(fullPathUserGroupId, GroupType.EXTERNAL, List.of()); + var vaultUserGroup = this.vaultAdminClient.acl().getGroupByName(fullPathUserGroupId); + var mountAccessor = this.vaultAdminClient.auth().getJwtAuthMethod().getAccessor(); try { - vaultAdminClient.auth().addJwtGroupAlias( - fullPathUserGroupId, - mountAccessor, - vaultUserGroup.getId()); + this.vaultAdminClient.auth().addJwtGroupAlias(fullPathUserGroupId, mountAccessor, vaultUserGroup.getId()); } catch (VaultRuntimeException e) { - if (e.getCause().getMessage().contains("combination of mount and group alias name is already in use")) { - // Group alias already exists, ignore - } else { + if (!e.getCause().getMessage().contains("combination of mount and group alias name is already in use")) { throw e; } } } + + @Override + public List> getUsers() { + var realm = this.multiTenantKeycloakRegistration.getDefaultRealm(); + var adminUserIds = new HashSet<>(this.keycloakAdminClient.getUserIdsAssignedToRole(KEYCLOAK_REALM_ROLE_NAME_SLM_ADMIN)); + + return this.keycloakAdminClient.getUsersOfRealm(realm) + .stream() + .filter(user -> user.getUsername() != null && !user.getUsername().startsWith("service-account-")) + .map(user -> Map.of( + "username", user.getUsername(), + "firstName", Objects.requireNonNullElse(user.getFirstName(), ""), + "lastName", Objects.requireNonNullElse(user.getLastName(), ""), + "email", Objects.requireNonNullElse(user.getEmail(), ""), + "admin", adminUserIds.contains(user.getId()))) + .toList(); + } + + @Override + public void makeUserAdmin(String username) { + try { + var userId = this.keycloakAdminClient.getUserId(username); + this.keycloakAdminClient.assignUserToRealmRole(KEYCLOAK_REALM_ROLE_NAME_SLM_ADMIN, userId); + LOG.info("Successfully granted '{}' role to user '{}'", KEYCLOAK_REALM_ROLE_NAME_SLM_ADMIN, username); + } catch (Exception e) { + if (e instanceof KeycloakUserNotFoundException) { + throw new UserNotFoundException("User '" + username + "' not found", e); + } + throw new UserManagementRuntimeException( + "Error assigning role '" + KEYCLOAK_REALM_ROLE_NAME_SLM_ADMIN + "' to user '" + username + "'", + e); + } + } } + diff --git a/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserNotFoundException.kt b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/UserNotFoundException.kt similarity index 73% rename from platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserNotFoundException.kt rename to platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/UserNotFoundException.kt index 9ca500975..9a64eccd8 100644 --- a/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserNotFoundException.kt +++ b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/main/java/org/eclipse/slm/platform_management/features/user_management/impl/UserNotFoundException.kt @@ -1,11 +1,11 @@ -package org.eclipse.slm.platform_management.service.app.users +package org.eclipse.slm.platform_management.features.user_management.impl import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.ResponseStatus -/** Exception thrown when a user is not found. */ @ResponseStatus(HttpStatus.NOT_FOUND) class UserNotFoundException : RuntimeException { constructor(message: String) : super(message) constructor(message: String, cause: Throwable) : super(message, cause) -} \ No newline at end of file +} + diff --git a/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/test/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManagementRestControllerTest.java b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/test/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManagementRestControllerTest.java new file mode 100644 index 000000000..f13448221 --- /dev/null +++ b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/test/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManagementRestControllerTest.java @@ -0,0 +1,98 @@ +package org.eclipse.slm.platform_management.features.user_management.impl; + +import org.eclipse.slm.platform_management.features.user_management.api.UserCreateRequest; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class UserManagementRestControllerTest { + + @Mock + private UserManager userManager; + + @Nested + class GetUsersTests { + + @Test + void getUsers_returnsOkAndDelegatesToManager() { + var controller = new UserManagementRestController(userManager); + var users = List.of(Map.of( + "username", "alice", + "firstName", "Alice", + "lastName", "Doe", + "email", "alice@example.org", + "admin", true + )); + when(userManager.getUsers()).thenReturn(users); + + var response = controller.getUsers(); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(users, response.getBody()); + verify(userManager).getUsers(); + } + } + + @Nested + class CreateUserTests { + + @Test + void createUser_returnsOkAndDelegatesToManager() { + var controller = new UserManagementRestController(userManager); + var request = new UserCreateRequest( + "Alice_Admin", + "Alice", + "Doe", + "secret", + false, + "alice@example.org", + false + ); + + var response = controller.createUser(request); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + verify(userManager).createUser(request); + } + } + + @Nested + class MakeUserAdminTests { + + @Test + void makeUserAdmin_returnsOkAndDelegatesToManager() { + var controller = new UserManagementRestController(userManager); + + var response = controller.makeUserAdmin("alice"); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + verify(userManager).makeUserAdmin("alice"); + } + } + + @Nested + class DeleteUserTests { + + @Test + void deleteUser_returnsOkAndDelegatesToManager() { + var controller = new UserManagementRestController(userManager); + + var response = controller.deleteUser("alice"); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + verify(userManager).deleteUser("alice"); + } + } +} + diff --git a/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/test/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManagerImplTest.java b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/test/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManagerImplTest.java new file mode 100644 index 000000000..f2a170025 --- /dev/null +++ b/platform_management/platform_management.features/platform_management.features.user_management/platform_management.features.user_management.impl/src/test/java/org/eclipse/slm/platform_management/features/user_management/impl/UserManagerImplTest.java @@ -0,0 +1,218 @@ +package org.eclipse.slm.platform_management.features.user_management.impl; + +import org.eclipse.slm.common.consul.client.ConsulAclClient; +import org.eclipse.slm.common.consul.client.ConsulClient; +import org.eclipse.slm.common.consul.client.ConsulClientFactory; +import org.eclipse.slm.common.consul.model.acl.bindingrules.BindingRule; +import org.eclipse.slm.common.consul.model.acl.roles.Role; +import org.eclipse.slm.common.keycloak.config.KeycloakAdminClient; +import org.eclipse.slm.common.keycloak.config.MultiTenantKeycloakRegistration; +import org.eclipse.slm.common.keycloak.config.exceptions.KeycloakUserNotFoundException; +import org.eclipse.slm.common.vault.client.VaultClient; +import org.eclipse.slm.common.vault.client.VaultClientAcl; +import org.eclipse.slm.common.vault.client.VaultClientAuth; +import org.eclipse.slm.common.vault.client.VaultClientFactory; +import org.eclipse.slm.common.vault.model.acl.Group; +import org.eclipse.slm.common.vault.model.auth.JwtGroupAlias; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.keycloak.representations.idm.UserRepresentation; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class UserManagerImplTest { + + private static final String DEFAULT_REALM = "fabos"; + + @Mock + private MultiTenantKeycloakRegistration multiTenantKeycloakRegistration; + + @Mock + private ConsulClientFactory consulClientFactory; + + @Mock + private VaultClientFactory vaultClientFactory; + + @Mock + private KeycloakAdminClient keycloakAdminClient; + + @Mock + private ConsulClient consulClient; + + @Mock + private ConsulAclClient consulAclClient; + + @Mock + private VaultClient vaultClient; + + @Mock + private VaultClientAcl vaultClientAcl; + + @Mock + private VaultClientAuth vaultClientAuth; + + private UserManagerImpl userManager; + + @BeforeEach + void setUp() { + when(consulClientFactory.createAdminClient()).thenReturn(consulClient); + when(vaultClientFactory.createAdminClient()).thenReturn(vaultClient); + + userManager = new UserManagerImpl( + multiTenantKeycloakRegistration, + consulClientFactory, + vaultClientFactory, + keycloakAdminClient); + } + + @Nested + class DeleteUserTests { + + @BeforeEach + void setUpDeleteUserTests() { + when(multiTenantKeycloakRegistration.getDefaultRealm()).thenReturn(DEFAULT_REALM); + } + + @Test + void deleteUser_deletesResources_whenUserCanBeDeleted() throws Exception { + var username = "alice"; + var keycloakUserId = "user-1"; + var fullPathUserGroupId = "/users/user-1"; + var cleanedBindingRuleName = "users_user-1"; + + when(consulClient.acl()).thenReturn(consulAclClient); + when(vaultClient.acl()).thenReturn(vaultClientAcl); + when(vaultClient.auth()).thenReturn(vaultClientAuth); + + when(keycloakAdminClient.getUserId(username)).thenReturn(keycloakUserId); + when(keycloakAdminClient.getUserIdsAssignedToRole(UserManagerImpl.KEYCLOAK_REALM_ROLE_NAME_SLM_ADMIN)) + .thenReturn(List.of("another-admin")); + + var role = new Role("role-id", "roleName", null, Collections.emptyList()); + when(consulAclClient.getRoleByName(fullPathUserGroupId)).thenReturn(role); + when(consulAclClient.cleanUserGroupBindingRuleName(fullPathUserGroupId)).thenReturn(cleanedBindingRuleName); + when(consulAclClient.getBindingRules()).thenReturn(List.of( + new BindingRule("binding-rule-id", "desc", "keycloak", "selector", "role", cleanedBindingRuleName) + )); + + var vaultGroup = new Group(); + vaultGroup.setAlias(new JwtGroupAlias(null, null, "alias-id", null, null, null, null, null, null, null)); + when(vaultClientAcl.getGroupByName(fullPathUserGroupId)).thenReturn(vaultGroup); + + userManager.deleteUser(username); + + verify(consulAclClient).deleteRoleById("role-id"); + verify(consulAclClient).deleteBindingRuleById("binding-rule-id"); + verify(vaultClientAuth).removeJwtGroupAlias("alias-id"); + verify(vaultClientAcl).deleteGroupByName(fullPathUserGroupId); + verify(keycloakAdminClient).deleteChildGroup(DEFAULT_REALM, keycloakUserId, UserManagerImpl.KEYCLOAK_PARENT_GROUP_USERS); + verify(keycloakAdminClient).deleteUser(DEFAULT_REALM, keycloakUserId); + } + + @Test + void deleteUser_throwsLastAdminDeletionNotAllowedException_whenDeletingOnlyAdmin() throws Exception { + var username = "admin"; + var keycloakUserId = "admin-user-id"; + + when(keycloakAdminClient.getUserId(username)).thenReturn(keycloakUserId); + when(keycloakAdminClient.getUserIdsAssignedToRole(UserManagerImpl.KEYCLOAK_REALM_ROLE_NAME_SLM_ADMIN)) + .thenReturn(List.of(keycloakUserId)); + + var exception = assertThrows(LastAdminDeletionNotAllowedException.class, () -> userManager.deleteUser(username)); + + assertTrue(exception.getMessage().contains("at least one admin user must remain")); + verify(keycloakAdminClient, never()).deleteUser(DEFAULT_REALM, keycloakUserId); + } + + @Test + void deleteUser_throwsUserNotFoundException_whenKeycloakUserDoesNotExist() throws Exception { + var username = "ghost"; + when(keycloakAdminClient.getUserId(username)).thenThrow(new KeycloakUserNotFoundException(username)); + + var exception = assertThrows(UserNotFoundException.class, () -> userManager.deleteUser(username)); + + assertTrue(exception.getMessage().contains("User 'ghost' not found")); + } + } + + @Nested + class GetUsersTests { + + @BeforeEach + void setUpGetUsersTests() { + when(multiTenantKeycloakRegistration.getDefaultRealm()).thenReturn(DEFAULT_REALM); + } + + @Test + void getUsers_filtersServiceAccountsAndMapsAdminFlagAndFallbackValues() { + var regularUser = new UserRepresentation(); + regularUser.setId("user-1"); + regularUser.setUsername("alice"); + regularUser.setFirstName(null); + regularUser.setLastName("Doe"); + regularUser.setEmail(null); + + var serviceAccount = new UserRepresentation(); + serviceAccount.setId("user-2"); + serviceAccount.setUsername("service-account-operator"); + + when(keycloakAdminClient.getUserIdsAssignedToRole(UserManagerImpl.KEYCLOAK_REALM_ROLE_NAME_SLM_ADMIN)) + .thenReturn(List.of("user-1")); + when(keycloakAdminClient.getUsersOfRealm(DEFAULT_REALM)).thenReturn(List.of(regularUser, serviceAccount)); + + List> users = userManager.getUsers(); + + assertEquals(1, users.size()); + var userEntry = users.getFirst(); + assertEquals("alice", userEntry.get("username")); + assertEquals("", userEntry.get("firstName")); + assertEquals("Doe", userEntry.get("lastName")); + assertEquals("", userEntry.get("email")); + assertTrue((Boolean) userEntry.get("admin")); + } + } + + @Nested + class MakeUserAdminTests { + + @Test + void makeUserAdmin_assignsAdminRole_whenUserExists() throws Exception { + var username = "alice"; + var userId = "user-1"; + when(keycloakAdminClient.getUserId(username)).thenReturn(userId); + + userManager.makeUserAdmin(username); + + verify(keycloakAdminClient).assignUserToRealmRole(UserManagerImpl.KEYCLOAK_REALM_ROLE_NAME_SLM_ADMIN, userId); + } + + @Test + void makeUserAdmin_throwsUserNotFoundException_whenUserDoesNotExist() throws Exception { + var username = "ghost"; + when(keycloakAdminClient.getUserId(username)).thenThrow(new KeycloakUserNotFoundException(username)); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> userManager.makeUserAdmin(username)); + + assertInstanceOf(UserNotFoundException.class, exception); + assertFalse(exception.getMessage().isBlank()); + } + } +} + + diff --git a/platform_management/platform_management.features/platform_management.features.user_management/pom.xml b/platform_management/platform_management.features/platform_management.features.user_management/pom.xml new file mode 100644 index 000000000..882ad3f5a --- /dev/null +++ b/platform_management/platform_management.features/platform_management.features.user_management/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + platform_management.features.user_management + ${revision} + pom + + + org.eclipse.slm + platform_management.features + ${revision} + + + + platform_management.features.user_management.api + platform_management.features.user_management.impl + + + + diff --git a/platform_management/platform_management.features/pom.xml b/platform_management/platform_management.features/pom.xml new file mode 100644 index 000000000..aa4e6ef5e --- /dev/null +++ b/platform_management/platform_management.features/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + platform_management.features + pom + + + org.eclipse.slm + platform_management + ${revision} + + + + platform_management.features.credentials_management + platform_management.features.user_management + + + + diff --git a/platform_management/platform_management.service/platform_management.service.api/src/main/java/org/eclipse/slm/platform_management/service/api/credentials/CredentialManagementRestApiConfig.java b/platform_management/platform_management.service/platform_management.service.api/src/main/java/org/eclipse/slm/platform_management/service/api/credentials/CredentialManagementRestApiConfig.java deleted file mode 100644 index 3d1f2744f..000000000 --- a/platform_management/platform_management.service/platform_management.service.api/src/main/java/org/eclipse/slm/platform_management/service/api/credentials/CredentialManagementRestApiConfig.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.eclipse.slm.platform_management.service.api.credentials; - -public class CredentialManagementRestApiConfig { - - public static final String BASE_PATH = "/credentials"; - - public static final String TAG = "Credentials"; - -} diff --git a/platform_management/platform_management.service/platform_management.service.app/pom.xml b/platform_management/platform_management.service/platform_management.service.app/pom.xml index b560b2dbf..8dab45d2e 100644 --- a/platform_management/platform_management.service/platform_management.service.app/pom.xml +++ b/platform_management/platform_management.service/platform_management.service.app/pom.xml @@ -47,6 +47,19 @@ ${project.version} compile + + + org.eclipse.slm + platform_management.features.credentials_management.impl + ${project.version} + compile + + + org.eclipse.slm + platform_management.features.user_management.impl + ${project.version} + compile + diff --git a/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserCreateRequest.kt b/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserCreateRequest.kt deleted file mode 100644 index 27dfb5ad4..000000000 --- a/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserCreateRequest.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.eclipse.slm.platform_management.service.app.users - -import com.fasterxml.jackson.annotation.JsonProperty - -data class UserCreateRequest( - - @param:JsonProperty("username") - val username: String, - - @param:JsonProperty("firstName") - val firstName: String, - - @param:JsonProperty("lastName") - val lastName: String, - - @param:JsonProperty("password") - val password: String, - - @param:JsonProperty("isPasswordTemporary") - val isPasswordTemporary: Boolean = false, - - @param:JsonProperty("email") - val email: String, - - @param:JsonProperty("isAdmin") - val isAdmin: Boolean = false, - -) diff --git a/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManagementRestController.java b/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManagementRestController.java deleted file mode 100644 index dfdda2fee..000000000 --- a/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManagementRestController.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.eclipse.slm.platform_management.service.app.users; - -import io.swagger.v3.oas.annotations.tags.Tag; -import org.eclipse.slm.common.restserver.annotations.AuthorizedAsSlmAdminOrApiKey; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping(UserManagementRestApiConfig.BASE_PATH) -@Tag(name = UserManagementRestApiConfig.TAG) -public class UserManagementRestController implements UserManagementRestApi { - - private final UserManager userManager; - - public UserManagementRestController(UserManager userManager) { - this.userManager = userManager; - } - - @Override - @AuthorizedAsSlmAdminOrApiKey - public ResponseEntity createUser(UserCreateRequest userCreateRequest) { - var authentication = SecurityContextHolder.getContext().getAuthentication(); - - this.userManager.createUser(userCreateRequest); - return ResponseEntity.ok().build(); - } - - @Override - public ResponseEntity deleteUser(String username) { - this.userManager.deleteUser(username); - return ResponseEntity.ok().build(); - } -} diff --git a/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManager.java b/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManager.java deleted file mode 100644 index 8ad20aee0..000000000 --- a/platform_management/platform_management.service/platform_management.service.app/src/main/java/org/eclipse/slm/platform_management/service/app/users/UserManager.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.eclipse.slm.platform_management.service.app.users; - -public interface UserManager { - - void createUser(UserCreateRequest userCreateRequest); - - void deleteUser(String username); - -} diff --git a/platform_management/platform_management.service/platform_management.service.client/pom.xml b/platform_management/platform_management.service/platform_management.service.client/pom.xml index 359dcfbca..ee9fb2ef7 100644 --- a/platform_management/platform_management.service/platform_management.service.client/pom.xml +++ b/platform_management/platform_management.service/platform_management.service.client/pom.xml @@ -18,7 +18,7 @@ org.eclipse.slm - platform_management.service.api + platform_management.common.api ${project.version} compile diff --git a/platform_management/platform_management.service/platform_management.service.client/src/main/java/org/eclipse/slm/platform_management/service/client/PlatformManagementClient.java b/platform_management/platform_management.service/platform_management.service.client/src/main/java/org/eclipse/slm/platform_management/service/client/PlatformManagementClient.java index a9854b4f0..706cd2afc 100644 --- a/platform_management/platform_management.service/platform_management.service.client/src/main/java/org/eclipse/slm/platform_management/service/client/PlatformManagementClient.java +++ b/platform_management/platform_management.service/platform_management.service.client/src/main/java/org/eclipse/slm/platform_management/service/client/PlatformManagementClient.java @@ -2,7 +2,7 @@ import org.eclipse.slm.common.parent.client.AbstractApiClient; import org.eclipse.slm.common.restclient.feign.auth.AuthRequestInterceptor; -import org.eclipse.slm.platform_management.service.api.credentials.CredentialManagementRestApiConfig; +import org.eclipse.slm.platform_management.features.credentials_management.api.CredentialManagementRestApiConfig; import org.springframework.beans.factory.ObjectFactory; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; diff --git a/platform_management/platform_management.service/platform_management.service.client/src/main/java/org/eclipse/slm/platform_management/service/client/PlatformManagementCredentialsClient.java b/platform_management/platform_management.service/platform_management.service.client/src/main/java/org/eclipse/slm/platform_management/service/client/PlatformManagementCredentialsClient.java index 62c08cc5d..21effcc7f 100644 --- a/platform_management/platform_management.service/platform_management.service.client/src/main/java/org/eclipse/slm/platform_management/service/client/PlatformManagementCredentialsClient.java +++ b/platform_management/platform_management.service/platform_management.service.client/src/main/java/org/eclipse/slm/platform_management/service/client/PlatformManagementCredentialsClient.java @@ -1,8 +1,8 @@ package org.eclipse.slm.platform_management.service.client; import org.eclipse.slm.platform_management.service.api.PlatformManagementApiConfig; -import org.eclipse.slm.platform_management.service.api.credentials.CredentialManagementRestApi; -import org.eclipse.slm.platform_management.service.api.credentials.CredentialManagementRestApiConfig; +import org.eclipse.slm.platform_management.features.credentials_management.api.CredentialManagementRestApi; +import org.eclipse.slm.platform_management.features.credentials_management.api.CredentialManagementRestApiConfig; import org.springframework.cloud.openfeign.FeignClient; @FeignClient(name = "${platform-management.consul-service-name:platform-management}", path = PlatformManagementApiConfig.BASE_PATH + CredentialManagementRestApiConfig.BASE_PATH) diff --git a/platform_management/platform_management.service/pom.xml b/platform_management/platform_management.service/pom.xml index 7641f8398..8934e8d18 100644 --- a/platform_management/platform_management.service/pom.xml +++ b/platform_management/platform_management.service/pom.xml @@ -17,7 +17,6 @@ platform_management.service.app platform_management.service.client - platform_management.service.api \ No newline at end of file diff --git a/platform_management/pom.xml b/platform_management/pom.xml index 534f57702..190ce8c7c 100644 --- a/platform_management/pom.xml +++ b/platform_management/pom.xml @@ -15,6 +15,8 @@ + platform_management.common + platform_management.features platform_management.service diff --git a/resource_management/resource_management.common/pom.xml b/resource_management/resource_management.common/pom.xml index 6f2a31e17..8e8f28ed4 100644 --- a/resource_management/resource_management.common/pom.xml +++ b/resource_management/resource_management.common/pom.xml @@ -101,6 +101,13 @@ ${project.version} compile + + + org.eclipse.slm + common.utils.keycloak + ${project.version} + compile + org.eclipse.slm diff --git a/resource_management/resource_management.common/src/main/java/org/eclipse/slm/resource_management/common/credentials/ResourceCredentialsManager.java b/resource_management/resource_management.common/src/main/java/org/eclipse/slm/resource_management/common/credentials/ResourceCredentialsManager.java index ee3fbecfd..db60d0147 100644 --- a/resource_management/resource_management.common/src/main/java/org/eclipse/slm/resource_management/common/credentials/ResourceCredentialsManager.java +++ b/resource_management/resource_management.common/src/main/java/org/eclipse/slm/resource_management/common/credentials/ResourceCredentialsManager.java @@ -5,7 +5,7 @@ import org.eclipse.slm.common.credentials.model.CredentialData; import org.eclipse.slm.common.credentials.model.CredentialEntityLinkCreateDTO; import org.eclipse.slm.common.restclient.feign.FeignResponseException; -import org.eclipse.slm.platform_management.service.api.credentials.CredentialCreateRequest; +import org.eclipse.slm.platform_management.features.credentials_management.api.CredentialCreateRequest; import org.eclipse.slm.platform_management.service.client.PlatformManagementClient; import org.eclipse.slm.platform_management.service.client.PlatformManagementClientFactory; import org.eclipse.slm.platform_management.service.client.PlatformManagementCredentialsClient; diff --git a/resource_management/resource_management.common/src/main/java/org/eclipse/slm/resource_management/common/resources/ResourcesRestController.java b/resource_management/resource_management.common/src/main/java/org/eclipse/slm/resource_management/common/resources/ResourcesRestController.java index a89fcb216..11f2fb1f0 100644 --- a/resource_management/resource_management.common/src/main/java/org/eclipse/slm/resource_management/common/resources/ResourcesRestController.java +++ b/resource_management/resource_management.common/src/main/java/org/eclipse/slm/resource_management/common/resources/ResourcesRestController.java @@ -1,7 +1,6 @@ package org.eclipse.slm.resource_management.common.resources; import io.swagger.v3.oas.annotations.tags.Tag; -import org.eclipse.slm.common.restserver.annotations.AuthorizedAsSlmUser; import org.eclipse.slm.common.utils.keycloak.KeycloakTokenUtil; import org.eclipse.slm.resource_management.common.exceptions.ResourceDefinitionException; import org.eclipse.slm.resource_management.common.exceptions.ResourceNotFoundException; diff --git a/resource_management/resource_management.features/resource_management.features.device-integration/resource_management.features.device-integration.discovery/pom.xml b/resource_management/resource_management.features/resource_management.features.device-integration/resource_management.features.device-integration.discovery/pom.xml index 242e86bda..85bb22435 100644 --- a/resource_management/resource_management.features/resource_management.features.device-integration/resource_management.features.device-integration.discovery/pom.xml +++ b/resource_management/resource_management.features/resource_management.features.device-integration/resource_management.features.device-integration.discovery/pom.xml @@ -51,6 +51,13 @@ ${project.version} compile + + + org.eclipse.slm + common.utils.keycloak + ${project.version} + compile + org.eclipse.slm diff --git a/resource_management/resource_management.features/resource_management.features.device-integration/resource_management.features.device-integration.firmware-update/pom.xml b/resource_management/resource_management.features/resource_management.features.device-integration/resource_management.features.device-integration.firmware-update/pom.xml index 6356da595..e51136d2c 100644 --- a/resource_management/resource_management.features/resource_management.features.device-integration/resource_management.features.device-integration.firmware-update/pom.xml +++ b/resource_management/resource_management.features/resource_management.features.device-integration/resource_management.features.device-integration.firmware-update/pom.xml @@ -96,6 +96,12 @@ ${project.version} compile + + org.eclipse.slm + common.utils.keycloak + ${project.version} + compile + \ No newline at end of file diff --git a/resource_management/resource_management.features/resource_management.features.device-integration/resource_management.features.device-integration.firmware-update/src/main/java/org/eclipse/slm/resource_management/features/device_integration/firmware_update/FirmwareUpdatesRestController.java b/resource_management/resource_management.features/resource_management.features.device-integration/resource_management.features.device-integration.firmware-update/src/main/java/org/eclipse/slm/resource_management/features/device_integration/firmware_update/FirmwareUpdatesRestController.java index 3d114c7da..933a4b7c1 100644 --- a/resource_management/resource_management.features/resource_management.features.device-integration/resource_management.features.device-integration.firmware-update/src/main/java/org/eclipse/slm/resource_management/features/device_integration/firmware_update/FirmwareUpdatesRestController.java +++ b/resource_management/resource_management.features/resource_management.features.device-integration/resource_management.features.device-integration.firmware-update/src/main/java/org/eclipse/slm/resource_management/features/device_integration/firmware_update/FirmwareUpdatesRestController.java @@ -4,7 +4,6 @@ import org.apache.commons.io.IOUtils; import org.eclipse.digitaltwin.basyx.http.Base64UrlEncodedIdentifier; import org.eclipse.slm.common.minio.model.exceptions.*; -import org.eclipse.slm.common.restserver.annotations.AuthorizedAsSlmUser; import org.eclipse.slm.common.utils.general.Base64Util; import org.eclipse.slm.common.utils.keycloak.KeycloakTokenUtil; import org.eclipse.slm.resource_management.common.exceptions.ResourceTypeNotFoundException; diff --git a/resource_management/resource_management.features/resource_management.features.importer/src/main/java/org/eclipse/slm/resource_management/features/importer/ImporterService.java b/resource_management/resource_management.features/resource_management.features.importer/src/main/java/org/eclipse/slm/resource_management/features/importer/ImporterService.java index 3475a8fcd..e1511d9bd 100644 --- a/resource_management/resource_management.features/resource_management.features.importer/src/main/java/org/eclipse/slm/resource_management/features/importer/ImporterService.java +++ b/resource_management/resource_management.features/resource_management.features.importer/src/main/java/org/eclipse/slm/resource_management/features/importer/ImporterService.java @@ -3,7 +3,7 @@ import org.eclipse.slm.common.credentials.model.Credential; import org.eclipse.slm.common.credentials.model.CredentialDataUsernamePassword; import org.eclipse.slm.common.utils.keycloak.KeycloakTokenUtil; -import org.eclipse.slm.platform_management.service.api.credentials.CredentialCreateRequest; +import org.eclipse.slm.platform_management.features.credentials_management.api.CredentialCreateRequest; import org.eclipse.slm.platform_management.service.client.PlatformManagementClientFactory; import org.eclipse.slm.resource_management.common.aas.ResourceAas; import org.eclipse.slm.resource_management.common.aas.ResourcesSubmodelManager; diff --git a/resource_management/resource_management.service/resource_management.service.app/pom.xml b/resource_management/resource_management.service/resource_management.service.app/pom.xml index 9018248a8..41e5a9e6f 100644 --- a/resource_management/resource_management.service/resource_management.service.app/pom.xml +++ b/resource_management/resource_management.service/resource_management.service.app/pom.xml @@ -150,6 +150,12 @@ ${project.version} compile + + org.eclipse.slm + common.utils.keycloak + ${project.version} + compile + org.eclipse.slm common.utils.objectmapper diff --git a/ui/src/api/platform-management/platform-management-client.ts b/ui/src/api/platform-management/platform-management-client.ts index 8cf86d603..c653b0e11 100644 --- a/ui/src/api/platform-management/platform-management-client.ts +++ b/ui/src/api/platform-management/platform-management-client.ts @@ -1,13 +1,46 @@ +import axios from "axios"; import { CredentialsApi, + type UserCreateRequest, } from "@/api/platform-management/client"; +export interface UserOverviewResponse { + username: string; + firstName: string; + lastName: string; + email: string; + admin: boolean; +} class PlatformManagementClient{ apiUrl = "/platform-management"; credentialsApi = new CredentialsApi(undefined, this.apiUrl); + + async getUsers(): Promise { + return axios + .get(`${this.apiUrl}/users`) + .then(response => response.data); + } + + async createUser(userCreateRequest: UserCreateRequest): Promise { + return axios + .post(`${this.apiUrl}/users`, userCreateRequest) + .then(() => undefined); + } + + async makeUserAdmin(username: string): Promise { + return axios + .post(`${this.apiUrl}/users/${encodeURIComponent(username)}/admin`) + .then(() => undefined); + } + + async deleteUser(username: string): Promise { + return axios + .delete(`${this.apiUrl}/users/${encodeURIComponent(username)}`) + .then(() => undefined); + } } export default new PlatformManagementClient() \ No newline at end of file diff --git a/ui/src/components/core/Drawer.vue b/ui/src/components/core/Drawer.vue index 8614b1fbd..56410d0a5 100644 --- a/ui/src/components/core/Drawer.vue +++ b/ui/src/components/core/Drawer.vue @@ -282,6 +282,13 @@ export default { icon: 'mdi-treasure-chest', to: 'service-vendors', visible: this.userStore.userRoles.includes('slm-admin'), + }, + { + id: 'main-menu-button-admin-users', + title: 'User Management', + icon: 'mdi-account-cog', + to: 'users', + visible: this.userStore.userRoles.includes('slm-admin'), } ], }, diff --git a/ui/src/pages/AdminUsersPage.vue b/ui/src/pages/AdminUsersPage.vue new file mode 100644 index 000000000..05eb30102 --- /dev/null +++ b/ui/src/pages/AdminUsersPage.vue @@ -0,0 +1,367 @@ + + + + + + diff --git a/ui/src/pages/router.js b/ui/src/pages/router.js index be2029745..8c8a32621 100644 --- a/ui/src/pages/router.js +++ b/ui/src/pages/router.js @@ -183,6 +183,12 @@ const routes = [ component: () => import('@/pages/AdminServiceVendorsPage.vue'), meta: { adminPermissionRequired: true }, }, + { + name: 'AdminUsers', + path: '/admin/users', + component: () => import('@/pages/AdminUsersPage.vue'), + meta: { adminPermissionRequired: true }, + }, ], } ];