diff --git a/components/permissions/permission-rest-resource-impl/src/main/kotlin/net/corda/libs/permissions/endpoints/v1/user/impl/UserEndpointImpl.kt b/components/permissions/permission-rest-resource-impl/src/main/kotlin/net/corda/libs/permissions/endpoints/v1/user/impl/UserEndpointImpl.kt index ebc4bb132a5..9f48717d08c 100644 --- a/components/permissions/permission-rest-resource-impl/src/main/kotlin/net/corda/libs/permissions/endpoints/v1/user/impl/UserEndpointImpl.kt +++ b/components/permissions/permission-rest-resource-impl/src/main/kotlin/net/corda/libs/permissions/endpoints/v1/user/impl/UserEndpointImpl.kt @@ -15,6 +15,7 @@ import net.corda.libs.permissions.endpoints.v1.user.types.UserResponseType import net.corda.libs.permissions.manager.PermissionManager import net.corda.libs.permissions.manager.request.AddPropertyToUserRequestDto import net.corda.libs.permissions.manager.request.AddRoleToUserRequestDto +import net.corda.libs.permissions.manager.request.ChangeUserParentIdDto import net.corda.libs.permissions.manager.request.ChangeUserPasswordDto import net.corda.libs.permissions.manager.request.DeleteUserRequestDto import net.corda.libs.permissions.manager.request.GetPermissionSummaryRequestDto @@ -161,6 +162,23 @@ class UserEndpointImpl @Activate constructor( return userResponseDto?.convertToEndpointType() ?: throw ResourceNotFoundException("User", loginName) } + override fun changeUserParentGroup(loginName: String, newParentGroupId: String?): ResponseEntity { + val principal = getRestThreadLocalContext() + + val userResponseDto = withPermissionManager(permissionManagementService.permissionManager, logger) { + try { + changeUserParentGroup(ChangeUserParentIdDto(principal, loginName, newParentGroupId)) + } catch (e: NoSuchElementException) { + throw ResourceNotFoundException( + e::class.java.simpleName, + ExceptionDetails(e::class.java.name, e.message ?: "No resource found for this request.") + ) + } + } + + return ResponseEntity.updated(userResponseDto.convertToEndpointType()) + } + override fun changeUserPasswordSelf(password: String): UserResponseType { val principal = getRestThreadLocalContext() diff --git a/components/permissions/permission-rest-resource-impl/src/test/kotlin/net/corda/libs/permissions/endpoints/v1/user/impl/UserEndpointImplTest.kt b/components/permissions/permission-rest-resource-impl/src/test/kotlin/net/corda/libs/permissions/endpoints/v1/user/impl/UserEndpointImplTest.kt index 8678e805981..3cd24d7943a 100644 --- a/components/permissions/permission-rest-resource-impl/src/test/kotlin/net/corda/libs/permissions/endpoints/v1/user/impl/UserEndpointImplTest.kt +++ b/components/permissions/permission-rest-resource-impl/src/test/kotlin/net/corda/libs/permissions/endpoints/v1/user/impl/UserEndpointImplTest.kt @@ -4,6 +4,7 @@ import net.corda.libs.permissions.endpoints.v1.user.types.CreateUserType import net.corda.libs.permissions.manager.PermissionManager import net.corda.libs.permissions.manager.request.AddPropertyToUserRequestDto import net.corda.libs.permissions.manager.request.AddRoleToUserRequestDto +import net.corda.libs.permissions.manager.request.ChangeUserParentIdDto import net.corda.libs.permissions.manager.request.CreateUserRequestDto import net.corda.libs.permissions.manager.request.DeleteUserRequestDto import net.corda.libs.permissions.manager.request.GetUserPropertiesRequestDto @@ -201,6 +202,29 @@ internal class UserEndpointImplTest { assertEquals("abc", getUserRequestDtoCapture.firstValue.loginName) } + @Test + fun `change a user's parent group successfully`() { + val changeUserParentIdDtoCapture = argumentCaptor() + whenever(lifecycleCoordinator.isRunning).thenReturn(true) + whenever(permissionService.isRunning).thenReturn(true) + whenever(permissionManager.changeUserParentGroup(changeUserParentIdDtoCapture.capture())).thenReturn(userResponseDto) + + endpoint.start() + val response = endpoint.changeUserParentGroup("loginName1", parentGroup) + val responseType = response.responseBody + + assertEquals(ResponseCode.OK, response.responseCode) + assertNotNull(responseType) + assertEquals("uuid", responseType.id) + assertEquals(0, responseType.version) + assertEquals(now, responseType.updateTimestamp) + assertEquals("fullName1", responseType.fullName) + assertEquals("loginName1", responseType.loginName) + assertEquals(true, responseType.enabled) + assertEquals(now, responseType.passwordExpiry) + assertEquals(parentGroup, responseType.parentGroup) + } + @Test fun `add role to user`() { val userResponseDtoWithRole = UserResponseDto( diff --git a/gradle.properties b/gradle.properties index b2a9ed26d52..470c30b8f09 100644 --- a/gradle.properties +++ b/gradle.properties @@ -39,7 +39,7 @@ commonsLangVersion = 3.12.0 commonsTextVersion = 1.10.0 # Corda API libs revision (change in 4th digit indicates a breaking change) # Change to 5.3.0.xx-SNAPSHOT to pick up maven local published copy -cordaApiVersion=5.3.0.15-beta+ +cordaApiVersion=5.3.0.16-beta+ disruptorVersion=3.4.4 felixConfigAdminVersion=1.9.26 diff --git a/libs/corda-sdk/src/main/kotlin/net/corda/sdk/bootstrap/rbac/Permissions.kt b/libs/corda-sdk/src/main/kotlin/net/corda/sdk/bootstrap/rbac/Permissions.kt index 27814ec7b48..414bbf3784d 100644 --- a/libs/corda-sdk/src/main/kotlin/net/corda/sdk/bootstrap/rbac/Permissions.kt +++ b/libs/corda-sdk/src/main/kotlin/net/corda/sdk/bootstrap/rbac/Permissions.kt @@ -63,6 +63,7 @@ object Permissions { "CreateUser" to "POST:/api/$VERSION_PATH_REGEX/user", "GetUser" to "GET:/api/$VERSION_PATH_REGEX/user/${RbacKeys.USER_URL_REGEX}", "DeleteUser" to "DELETE:/api/$VERSION_PATH_REGEX/user/${RbacKeys.USER_URL_REGEX}", + "ChangeUserGroupParentId" to "PUT:/api/$VERSION_PATH_REGEX/user/${RbacKeys.USER_URL_REGEX}/parent/changeparentid/$UUID_REGEX", "ChangeOtherUserPassword" to "POST:/api/$VERSION_PATH_REGEX/user/otheruserpassword", "AddRoleToUser" to "PUT:/api/$VERSION_PATH_REGEX/user/${RbacKeys.USER_URL_REGEX}/role/$UUID_REGEX", "DeleteRoleFromUser" to "DELETE:/api/$VERSION_PATH_REGEX/user/${RbacKeys.USER_URL_REGEX}/role/$UUID_REGEX", @@ -90,7 +91,7 @@ object Permissions { // Group manipulation permissions "CreateGroup" to "POST:/api/$VERSION_PATH_REGEX/group", "GetGroup" to "GET:/api/$VERSION_PATH_REGEX/group/$UUID_REGEX", - "ChangeGroupParentId" to "PUT:/api/$VERSION_PATH_REGEX/group/$UUID_REGEX/parent/changeParentId/$UUID_REGEX", + "ChangeGroupParentId" to "PUT:/api/$VERSION_PATH_REGEX/group/$UUID_REGEX/parent/changeparentid/$UUID_REGEX", "AddRoleToGroup" to "PUT:/api/$VERSION_PATH_REGEX/group/$UUID_REGEX/role/$UUID_REGEX", "DeleteRoleFromGroup" to "DELETE:/api/$VERSION_PATH_REGEX/group/$UUID_REGEX/role/$UUID_REGEX", "DeleteGroup" to "DELETE:/api/$VERSION_PATH_REGEX/group/$UUID_REGEX" diff --git a/libs/permissions/permission-endpoint/src/main/kotlin/net/corda/libs/permissions/endpoints/v1/user/UserEndpoint.kt b/libs/permissions/permission-endpoint/src/main/kotlin/net/corda/libs/permissions/endpoints/v1/user/UserEndpoint.kt index ef296e213f2..874250a14b6 100644 --- a/libs/permissions/permission-endpoint/src/main/kotlin/net/corda/libs/permissions/endpoints/v1/user/UserEndpoint.kt +++ b/libs/permissions/permission-endpoint/src/main/kotlin/net/corda/libs/permissions/endpoints/v1/user/UserEndpoint.kt @@ -120,6 +120,34 @@ interface UserEndpoint : RestResource { loginName: String ): ResponseEntity + @HttpPUT( + path = "{loginName}/parent/changeparentid/{newParentGroupId}", + description = "This method changes the parent group of a specified user.", + responseDescription = """ + The user with the updated parent group with the following attributes: + id: Unique server generated identifier for the user + version: The version of the user; version 0 is assigned to a newly created user + updateTimestamp: The date and time when the user was last updated + fullName: The full name for the new user + loginName: The login name for the new user + enabled: If true, the user account is enabled; false, the account is disabled + ssoAuth: If true, the user account is enabled for SSO authentication; + false, the account is enabled for password authentication + passwordExpiry: The date and time when the password should expire, specified as an ISO-8601 string; + value of null means that the password does not expire + parentGroup: An optional identifier of the user group for the new user to be included; + value of null means that the user will belong to the root group + properties: An optional set of key/value properties associated with a user account + roleAssociations: A set of roles associated with the user account""", + minVersion = RestApiVersion.C5_3 + ) + fun changeUserParentGroup( + @RestPathParameter(description = "ID of the user to change parent group.") + loginName: String, + @RestPathParameter(description = "New parent group ID.") + newParentGroupId: String? + ): ResponseEntity + @HttpPOST( path = "/selfpassword", description = "This method updates a users own password.", diff --git a/libs/permissions/permission-manager-impl/src/main/kotlin/net/corda/libs/permissions/manager/impl/PermissionUserManagerImpl.kt b/libs/permissions/permission-manager-impl/src/main/kotlin/net/corda/libs/permissions/manager/impl/PermissionUserManagerImpl.kt index bcdd5dee4cc..5887255ab11 100644 --- a/libs/permissions/permission-manager-impl/src/main/kotlin/net/corda/libs/permissions/manager/impl/PermissionUserManagerImpl.kt +++ b/libs/permissions/permission-manager-impl/src/main/kotlin/net/corda/libs/permissions/manager/impl/PermissionUserManagerImpl.kt @@ -5,6 +5,7 @@ import net.corda.data.permissions.management.PermissionManagementRequest import net.corda.data.permissions.management.PermissionManagementResponse import net.corda.data.permissions.management.user.AddPropertyToUserRequest import net.corda.data.permissions.management.user.AddRoleToUserRequest +import net.corda.data.permissions.management.user.ChangeUserParentGroupIdRequest import net.corda.data.permissions.management.user.ChangeUserPasswordRequest import net.corda.data.permissions.management.user.CreateUserRequest import net.corda.data.permissions.management.user.DeleteUserRequest @@ -18,6 +19,7 @@ import net.corda.libs.permissions.manager.impl.SmartConfigUtil.getEndpointTimeou import net.corda.libs.permissions.manager.impl.converter.convertToResponseDto import net.corda.libs.permissions.manager.request.AddPropertyToUserRequestDto import net.corda.libs.permissions.manager.request.AddRoleToUserRequestDto +import net.corda.libs.permissions.manager.request.ChangeUserParentIdDto import net.corda.libs.permissions.manager.request.ChangeUserPasswordDto import net.corda.libs.permissions.manager.request.CreateUserRequestDto import net.corda.libs.permissions.manager.request.DeleteUserRequestDto @@ -104,6 +106,30 @@ class PermissionUserManagerImpl( return result.convertToResponseDto() } + override fun changeUserParentGroup(changeUserParentGroupIdDto: ChangeUserParentIdDto): UserResponseDto { + if (changeUserParentGroupIdDto.newParentGroupId != null) { + val permissionManagementCache = checkNotNull(permissionManagementCacheRef.get()) { + "Permission management cache is null." + } + permissionManagementCache.getGroup(changeUserParentGroupIdDto.newParentGroupId!!) + ?: throw NoSuchElementException("Could not find user with parent group id ${changeUserParentGroupIdDto.newParentGroupId}") + } + + val result = sendPermissionWriteRequest( + rpcSender, + writerTimeout, + PermissionManagementRequest( + changeUserParentGroupIdDto.requestedBy, + null, + ChangeUserParentGroupIdRequest( + changeUserParentGroupIdDto.loginName, + changeUserParentGroupIdDto.newParentGroupId, + ) + ) + ) + return result.convertToResponseDto() + } + override fun changeUserPasswordSelf(changeUserPasswordDto: ChangeUserPasswordDto): UserResponseDto = changeUserPassword(changeUserPasswordDto, selfUserPasswordExpiryDays) diff --git a/libs/permissions/permission-manager-impl/src/test/kotlin/net/corda/libs/permissions/manager/impl/PermissionUserManagerImplTest.kt b/libs/permissions/permission-manager-impl/src/test/kotlin/net/corda/libs/permissions/manager/impl/PermissionUserManagerImplTest.kt index 7bf8540650b..e6efaa21935 100644 --- a/libs/permissions/permission-manager-impl/src/test/kotlin/net/corda/libs/permissions/manager/impl/PermissionUserManagerImplTest.kt +++ b/libs/permissions/permission-manager-impl/src/test/kotlin/net/corda/libs/permissions/manager/impl/PermissionUserManagerImplTest.kt @@ -2,6 +2,7 @@ package net.corda.libs.permissions.manager.impl import com.typesafe.config.ConfigValueFactory import net.corda.data.permissions.ChangeDetails +import net.corda.data.permissions.Group import net.corda.data.permissions.Property import net.corda.data.permissions.RoleAssociation import net.corda.data.permissions.User @@ -9,6 +10,7 @@ import net.corda.data.permissions.management.PermissionManagementRequest import net.corda.data.permissions.management.PermissionManagementResponse import net.corda.data.permissions.management.user.AddPropertyToUserRequest import net.corda.data.permissions.management.user.AddRoleToUserRequest +import net.corda.data.permissions.management.user.ChangeUserParentGroupIdRequest import net.corda.data.permissions.management.user.CreateUserRequest import net.corda.data.permissions.management.user.DeleteUserRequest import net.corda.data.permissions.management.user.RemovePropertyFromUserRequest @@ -20,6 +22,7 @@ import net.corda.libs.permissions.management.cache.PermissionManagementCache import net.corda.libs.permissions.manager.exception.UnexpectedPermissionResponseException import net.corda.libs.permissions.manager.request.AddPropertyToUserRequestDto import net.corda.libs.permissions.manager.request.AddRoleToUserRequestDto +import net.corda.libs.permissions.manager.request.ChangeUserParentIdDto import net.corda.libs.permissions.manager.request.ChangeUserPasswordDto import net.corda.libs.permissions.manager.request.CreateUserRequestDto import net.corda.libs.permissions.manager.request.DeleteUserRequestDto @@ -39,6 +42,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows @@ -49,7 +53,6 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import java.lang.IllegalArgumentException import java.time.Duration import java.time.Instant import java.util.UUID @@ -293,6 +296,53 @@ class PermissionUserManagerImplTest { assertNull(result) } + @Test + fun `change user's parent group sends rpc request and converts result to response dto`() { + val loginName = UUID.randomUUID().toString() + val newParentGroupId = UUID.randomUUID().toString() + val avroUser = User( + "userId", 0, + ChangeDetails( + Instant.now() + ), + loginName, "fullName", true, "hashedPass", "salt", Instant.now(), false, newParentGroupId, emptyList(), emptyList() + ) + val avroGroup = Group( + UUID.randomUUID().toString(), + 0, + ChangeDetails(Instant.now()), + "groupName", + newParentGroupId, + emptyList(), + emptyList() + ) + + val future = mock>() + whenever(future.getOrThrow(defaultTimeout)).thenReturn(PermissionManagementResponse(avroUser)) + whenever(permissionManagementCache.getUser(loginName)).thenReturn(avroUser) + whenever(permissionManagementCache.getGroup(newParentGroupId)).thenReturn(avroGroup) + + val requestCaptor = argumentCaptor() + whenever(rpcSender.sendRequest(requestCaptor.capture())).thenReturn(future) + + val changeUserParentIdDto = ChangeUserParentIdDto("requestedBy", loginName, newParentGroupId) + val result = manager.changeUserParentGroup(changeUserParentIdDto) + + val capturedPermissionManagementRequest = requestCaptor.firstValue + assertEquals("requestedBy", capturedPermissionManagementRequest.requestUserId) + assertNull(capturedPermissionManagementRequest.virtualNodeId) + + val capturedChangeUserParentGroupIdRequest = capturedPermissionManagementRequest.request as ChangeUserParentGroupIdRequest + assertEquals(loginName, capturedChangeUserParentGroupIdRequest.loginName) + assertEquals(newParentGroupId, capturedChangeUserParentGroupIdRequest.newParentGroupId) + + assertEquals("userId", result.id) + assertEquals(loginName, result.loginName) + assertEquals(newParentGroupId, result.parentGroup) + assertTrue(result.properties.isEmpty()) + assertTrue(result.roles.isEmpty()) + } + @Test fun `changeUserPasswordSelf fails if the new password is the same as the existing one`() { whenever(permissionManagementCache.getUser("loginname123")).thenReturn(avroUser) diff --git a/libs/permissions/permission-manager/src/main/kotlin/net/corda/libs/permissions/manager/PermissionUserManager.kt b/libs/permissions/permission-manager/src/main/kotlin/net/corda/libs/permissions/manager/PermissionUserManager.kt index 9b2276fad43..07661d6dfa8 100644 --- a/libs/permissions/permission-manager/src/main/kotlin/net/corda/libs/permissions/manager/PermissionUserManager.kt +++ b/libs/permissions/permission-manager/src/main/kotlin/net/corda/libs/permissions/manager/PermissionUserManager.kt @@ -2,6 +2,7 @@ package net.corda.libs.permissions.manager import net.corda.libs.permissions.manager.request.AddPropertyToUserRequestDto import net.corda.libs.permissions.manager.request.AddRoleToUserRequestDto +import net.corda.libs.permissions.manager.request.ChangeUserParentIdDto import net.corda.libs.permissions.manager.request.ChangeUserPasswordDto import net.corda.libs.permissions.manager.request.CreateUserRequestDto import net.corda.libs.permissions.manager.request.DeleteUserRequestDto @@ -34,6 +35,11 @@ interface PermissionUserManager { */ fun deleteUser(deleteUserRequestDto: DeleteUserRequestDto): UserResponseDto + /** + * Change the parent group of a user in the RBAC Permission System. + */ + fun changeUserParentGroup(changeUserParentGroupIdDto: ChangeUserParentIdDto): UserResponseDto + /** * Change a user's own password. */ diff --git a/libs/permissions/permission-manager/src/main/kotlin/net/corda/libs/permissions/manager/request/ChangeUserParentIdDto.kt b/libs/permissions/permission-manager/src/main/kotlin/net/corda/libs/permissions/manager/request/ChangeUserParentIdDto.kt new file mode 100644 index 00000000000..d914800b48b --- /dev/null +++ b/libs/permissions/permission-manager/src/main/kotlin/net/corda/libs/permissions/manager/request/ChangeUserParentIdDto.kt @@ -0,0 +1,16 @@ +package net.corda.libs.permissions.manager.request + +data class ChangeUserParentIdDto( + /** + * ID of the user making the request. + */ + val requestedBy: String, + /** + * Login name of the User to change. + */ + val loginName: String, + /** + * ID of the new parent Group. + */ + val newParentGroupId: String? +) diff --git a/libs/permissions/permission-storage-writer-impl/src/main/kotlin/net/corda/libs/permissions/storage/writer/impl/PermissionStorageWriterProcessorImpl.kt b/libs/permissions/permission-storage-writer-impl/src/main/kotlin/net/corda/libs/permissions/storage/writer/impl/PermissionStorageWriterProcessorImpl.kt index 245361fbf57..08f9a9d5e53 100644 --- a/libs/permissions/permission-storage-writer-impl/src/main/kotlin/net/corda/libs/permissions/storage/writer/impl/PermissionStorageWriterProcessorImpl.kt +++ b/libs/permissions/permission-storage-writer-impl/src/main/kotlin/net/corda/libs/permissions/storage/writer/impl/PermissionStorageWriterProcessorImpl.kt @@ -17,6 +17,7 @@ import net.corda.data.permissions.management.role.CreateRoleRequest import net.corda.data.permissions.management.role.RemovePermissionFromRoleRequest import net.corda.data.permissions.management.user.AddPropertyToUserRequest import net.corda.data.permissions.management.user.AddRoleToUserRequest +import net.corda.data.permissions.management.user.ChangeUserParentGroupIdRequest import net.corda.data.permissions.management.user.ChangeUserPasswordRequest import net.corda.data.permissions.management.user.CreateUserRequest import net.corda.data.permissions.management.user.DeleteUserRequest @@ -66,6 +67,12 @@ class PermissionStorageWriterProcessorImpl( permissionStorageReader.publishNewRole(avroRole) avroRole } + is ChangeUserParentGroupIdRequest -> { + val avroUser = userWriter.changeUserParentGroup(permissionRequest, request.requestUserId) + permissionStorageReader.publishUpdatedUser(avroUser) + permissionStorageReader.reconcilePermissionSummaries() + avroUser + } is ChangeUserPasswordRequest -> { val avroUser = userWriter.changeUserPassword(permissionRequest, request.requestUserId) permissionStorageReader.publishUpdatedUser(avroUser) diff --git a/libs/permissions/permission-storage-writer-impl/src/main/kotlin/net/corda/libs/permissions/storage/writer/impl/user/UserWriter.kt b/libs/permissions/permission-storage-writer-impl/src/main/kotlin/net/corda/libs/permissions/storage/writer/impl/user/UserWriter.kt index 6435b140790..b9b821b3b68 100644 --- a/libs/permissions/permission-storage-writer-impl/src/main/kotlin/net/corda/libs/permissions/storage/writer/impl/user/UserWriter.kt +++ b/libs/permissions/permission-storage-writer-impl/src/main/kotlin/net/corda/libs/permissions/storage/writer/impl/user/UserWriter.kt @@ -2,6 +2,7 @@ package net.corda.libs.permissions.storage.writer.impl.user import net.corda.data.permissions.management.user.AddPropertyToUserRequest import net.corda.data.permissions.management.user.AddRoleToUserRequest +import net.corda.data.permissions.management.user.ChangeUserParentGroupIdRequest import net.corda.data.permissions.management.user.ChangeUserPasswordRequest import net.corda.data.permissions.management.user.CreateUserRequest import net.corda.data.permissions.management.user.DeleteUserRequest @@ -29,6 +30,14 @@ interface UserWriter { */ fun deleteUser(request: DeleteUserRequest, requestUserId: String): AvroUser + /** + * Change the parent group of a User entity and return its Avro representation. + * + * @param request ChangeUserParentGroupIdRequest containing the information of the User to change. + * @param requestUserId ID of the user who made the request. + */ + fun changeUserParentGroup(request: ChangeUserParentGroupIdRequest, requestUserId: String): AvroUser + /** * Change the password field of a User entity and return its Avro representation. * diff --git a/libs/permissions/permission-storage-writer-impl/src/main/kotlin/net/corda/libs/permissions/storage/writer/impl/user/impl/UserWriterImpl.kt b/libs/permissions/permission-storage-writer-impl/src/main/kotlin/net/corda/libs/permissions/storage/writer/impl/user/impl/UserWriterImpl.kt index 51780336aba..4f7ef64a06f 100644 --- a/libs/permissions/permission-storage-writer-impl/src/main/kotlin/net/corda/libs/permissions/storage/writer/impl/user/impl/UserWriterImpl.kt +++ b/libs/permissions/permission-storage-writer-impl/src/main/kotlin/net/corda/libs/permissions/storage/writer/impl/user/impl/UserWriterImpl.kt @@ -2,6 +2,7 @@ package net.corda.libs.permissions.storage.writer.impl.user.impl import net.corda.data.permissions.management.user.AddPropertyToUserRequest import net.corda.data.permissions.management.user.AddRoleToUserRequest +import net.corda.data.permissions.management.user.ChangeUserParentGroupIdRequest import net.corda.data.permissions.management.user.ChangeUserPasswordRequest import net.corda.data.permissions.management.user.CreateUserRequest import net.corda.data.permissions.management.user.DeleteUserRequest @@ -61,10 +62,39 @@ class UserWriterImpl( } } + override fun changeUserParentGroup( + request: ChangeUserParentGroupIdRequest, + requestUserId: String + ): AvroUser { + log.debug { "Received request to change parent group of User ${request.loginName} to ${request.newParentGroupId}" } + return entityManagerFactory.transaction { entityManager -> + + val validator = EntityValidationUtil(entityManager) + val user = validator.validateAndGetUniqueUser(request.loginName) + val newParentGroup = validator.validateAndGetUniqueGroup(request.newParentGroupId) + + user.parentGroup = newParentGroup + + val updateTimestamp = Instant.now() + val changeAudit = ChangeAudit( + id = UUID.randomUUID().toString(), + updateTimestamp = updateTimestamp, + actorUser = requestUserId, + changeType = RestPermissionOperation.USER_UPDATE, + details = "Parent group of User '${user.loginName}' changed to '${newParentGroup.id}' by '$requestUserId'." + ) + + entityManager.merge(user) + entityManager.persist(changeAudit) + + user.toAvroUser() + } + } + override fun changeUserPassword( request: ChangeUserPasswordRequest, requestUserId: String - ): net.corda.data.permissions.User { + ): AvroUser { log.debug { "Received request to change password for user: ${request.requestedBy}" } return entityManagerFactory.transaction { entityManager -> diff --git a/libs/permissions/permission-storage-writer-impl/src/test/kotlin/net/corda/libs/permissions/storage/writer/impl/user/UserWriterImplTest.kt b/libs/permissions/permission-storage-writer-impl/src/test/kotlin/net/corda/libs/permissions/storage/writer/impl/user/UserWriterImplTest.kt index c45fa3f0d10..0b307adb209 100644 --- a/libs/permissions/permission-storage-writer-impl/src/test/kotlin/net/corda/libs/permissions/storage/writer/impl/user/UserWriterImplTest.kt +++ b/libs/permissions/permission-storage-writer-impl/src/test/kotlin/net/corda/libs/permissions/storage/writer/impl/user/UserWriterImplTest.kt @@ -1,6 +1,7 @@ package net.corda.libs.permissions.storage.writer.impl.user import net.corda.data.permissions.management.user.AddRoleToUserRequest +import net.corda.data.permissions.management.user.ChangeUserParentGroupIdRequest import net.corda.data.permissions.management.user.ChangeUserPasswordRequest import net.corda.data.permissions.management.user.CreateUserRequest import net.corda.data.permissions.management.user.DeleteUserRequest @@ -58,6 +59,11 @@ internal class UserWriterImplTest { loginName = "lankydan" } + private val changeUserParentGroupIdRequest = ChangeUserParentGroupIdRequest().apply { + loginName = "userId1" + newParentGroupId = "parentId" + } + private val now = Instant.now() private val user = User("userId1", now, "user", "userLogin1", true, null, null, null, null) private val role = Role("role1", now, "roleName1", null) @@ -187,6 +193,73 @@ internal class UserWriterImplTest { assertEquals("User '${user.loginName}' deleted by '$requestUserId'.", audit.details) } + @Test + fun `changing parent group fails if user does not exist`() { + val typedQueryMock = mock>() + whenever(entityManager.createQuery(any(), eq(User::class.java))).thenReturn(typedQueryMock) + whenever(typedQueryMock.setParameter(eq("loginName"), eq(changeUserParentGroupIdRequest.loginName))) + .thenReturn(typedQueryMock) + whenever(typedQueryMock.resultList).thenReturn(emptyList()) + + val e = assertThrows { + userWriter.changeUserParentGroup(changeUserParentGroupIdRequest, requestUserId) + } + + assertEquals("User 'userId1' not found.", e.message) + } + + @Test + fun `changing parent group fails if parent group does not exist`() { + val typedQueryMock = mock>() + whenever(entityManager.createQuery(any(), eq(User::class.java))).thenReturn(typedQueryMock) + whenever(typedQueryMock.setParameter(eq("loginName"), eq(changeUserParentGroupIdRequest.loginName))) + .thenReturn(typedQueryMock) + whenever(typedQueryMock.resultList).thenReturn(listOf(user)) + + whenever(entityManager.find(Group::class.java, "parentId")).thenReturn(null) + + val e = assertThrows { + userWriter.changeUserParentGroup(changeUserParentGroupIdRequest, requestUserId) + } + + assertEquals("Group 'parentId' not found.", e.message) + } + + @Test + fun `changing parent group persists change to user and writes audit log`() { + val parentGroup = Group("parentId", Instant.now(), "parentGroupName", null) + + val typedQueryMock = mock>() + whenever(entityManager.createQuery(any(), eq(User::class.java))).thenReturn(typedQueryMock) + whenever(typedQueryMock.setParameter(eq("loginName"), eq(changeUserParentGroupIdRequest.loginName))) + .thenReturn(typedQueryMock) + whenever(typedQueryMock.resultList).thenReturn(listOf(user)) + + whenever(entityManager.find(Group::class.java, "parentId")).thenReturn(parentGroup) + + userWriter.changeUserParentGroup(changeUserParentGroupIdRequest, requestUserId) + + val userCaptor = argumentCaptor() + val auditCaptor = argumentCaptor() + + inOrder(entityTransaction, entityManager) { + verify(entityTransaction).begin() + verify(entityManager, times(1)).merge(userCaptor.capture()) + verify(entityManager, times(1)).persist(auditCaptor.capture()) + verify(entityTransaction).commit() + } + + val persistedUser = userCaptor.firstValue + assertNotNull(persistedUser) + assertEquals("userLogin1", persistedUser.loginName) + assertEquals(parentGroup, persistedUser.parentGroup) + + val audit = auditCaptor.firstValue + assertNotNull(audit) + assertEquals(RestPermissionOperation.USER_UPDATE, audit.changeType) + assertEquals("Parent group of User '${persistedUser.loginName}' changed to '${parentGroup.id}' by '$requestUserId'.", audit.details) + } + @Test fun `changing users own password successfully changes password`() { // Arrange diff --git a/processors/rest-processor/src/integrationTest/resources/swaggerBaseline-v5_3.json b/processors/rest-processor/src/integrationTest/resources/swaggerBaseline-v5_3.json index ac457919c10..144a089e5f2 100644 --- a/processors/rest-processor/src/integrationTest/resources/swaggerBaseline-v5_3.json +++ b/processors/rest-processor/src/integrationTest/resources/swaggerBaseline-v5_3.json @@ -3576,6 +3576,54 @@ } } }, + "/user/{loginname}/parent/changeparentid/{newparentgroupid}" : { + "put" : { + "tags" : [ "RBAC User" ], + "description" : "This method changes the parent group of a specified user.", + "operationId" : "put_user__loginname__parent_changeparentid__newparentgroupid_", + "parameters" : [ { + "name" : "loginname", + "in" : "path", + "description" : "ID of the user to change parent group.", + "required" : true, + "schema" : { + "type" : "string", + "description" : "ID of the user to change parent group.", + "nullable" : false, + "example" : "string" + } + }, { + "name" : "newparentgroupid", + "in" : "path", + "description" : "New parent group ID.", + "required" : true, + "schema" : { + "type" : "string", + "description" : "New parent group ID.", + "nullable" : true, + "example" : "string" + } + } ], + "responses" : { + "200" : { + "description" : "\n The user with the updated parent group with the following attributes:\n id: Unique server generated identifier for the user\n version: The version of the user; version 0 is assigned to a newly created user\n updateTimestamp: The date and time when the user was last updated\n fullName: The full name for the new user\n loginName: The login name for the new user\n enabled: If true, the user account is enabled; false, the account is disabled\n ssoAuth: If true, the user account is enabled for SSO authentication; \n false, the account is enabled for password authentication\n passwordExpiry: The date and time when the password should expire, specified as an ISO-8601 string;\n value of null means that the password does not expire\n parentGroup: An optional identifier of the user group for the new user to be included;\n value of null means that the user will belong to the root group\n properties: An optional set of key/value properties associated with a user account\n roleAssociations: A set of roles associated with the user account", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/UserResponseType" + } + } + } + }, + "401" : { + "description" : "Unauthorized" + }, + "403" : { + "description" : "Forbidden" + } + } + } + }, "/user/{loginname}/permissionsummary" : { "get" : { "tags" : [ "RBAC User" ], diff --git a/testing/e2e-test-utilities/src/main/kotlin/net/corda/e2etest/utilities/ClusterBuilder.kt b/testing/e2e-test-utilities/src/main/kotlin/net/corda/e2etest/utilities/ClusterBuilder.kt index 39ca5a86d29..25c7c646527 100644 --- a/testing/e2e-test-utilities/src/main/kotlin/net/corda/e2etest/utilities/ClusterBuilder.kt +++ b/testing/e2e-test-utilities/src/main/kotlin/net/corda/e2etest/utilities/ClusterBuilder.kt @@ -799,6 +799,12 @@ class ClusterBuilder(clusterInfo: ClusterInfo, val REST_API_VERSION_PATH: String initialClient.delete("/api/$REST_API_VERSION_PATH/user/$loginName") } + @Suppress("unused") + /** Change the parent group of a specified user */ + fun changeUserParentGroup(loginName: String, newParentGroupId: String?): SimpleResponse = trace("changeUserParentGroup") { + initialClient.put("/api/$REST_API_VERSION_PATH/user/$loginName/parent/changeparentid/$newParentGroupId", "") + } + @Suppress("unused") /** Assign a specified role to a specified user */ fun assignRoleToUser(loginName: String, roleId: String): SimpleResponse = trace("assignRoleToUser") {