Skip to content

Commit

Permalink
CORE-20731: Implement DeleteUser endpoint (#6224)
Browse files Browse the repository at this point in the history
  • Loading branch information
BenYip123 authored Jun 28, 2024
1 parent c8303a1 commit 1876501
Show file tree
Hide file tree
Showing 13 changed files with 237 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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.AddRoleToUserRequestDto
import net.corda.libs.permissions.manager.request.ChangeUserPasswordDto
import net.corda.libs.permissions.manager.request.DeleteUserRequestDto
import net.corda.libs.permissions.manager.request.GetPermissionSummaryRequestDto
import net.corda.libs.permissions.manager.request.GetRoleRequestDto
import net.corda.libs.permissions.manager.request.GetUserRequestDto
Expand Down Expand Up @@ -126,12 +127,18 @@ class UserEndpointImpl @Activate constructor(
return ResponseEntity.created(createUserResult.convertToEndpointType())
}

override fun getUserPath(loginName: String): UserResponseType {
return doGetUser(loginName)
override fun deleteUser(loginName: String): ResponseEntity<UserResponseType> {
val principal = getRestThreadLocalContext()

val userResponseDto = withPermissionManager(permissionManagementService.permissionManager, logger) {
deleteUser(DeleteUserRequestDto(principal, loginName.lowercase()))
}

return ResponseEntity.deleted(userResponseDto.convertToEndpointType())
}

override fun deleteUser(loginName: String): ResponseEntity<UserResponseType> {
TODO("To be implemented in CORE-20731")
override fun getUserPath(loginName: String): UserResponseType {
return doGetUser(loginName)
}

private fun doGetUser(loginName: String): UserResponseType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.AddRoleToUserRequestDto
import net.corda.libs.permissions.manager.request.CreateUserRequestDto
import net.corda.libs.permissions.manager.request.DeleteUserRequestDto
import net.corda.libs.permissions.manager.request.GetUserRequestDto
import net.corda.libs.permissions.manager.request.RemoveRoleFromUserRequestDto
import net.corda.libs.permissions.manager.response.RoleAssociationResponseDto
Expand All @@ -19,7 +20,7 @@ import net.corda.rest.exception.InvalidInputDataException
import net.corda.rest.exception.ResourceNotFoundException
import net.corda.rest.security.CURRENT_REST_CONTEXT
import net.corda.rest.security.RestAuthContext
import org.assertj.core.api.Assertions
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertTrue
Expand All @@ -30,7 +31,6 @@ import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import java.lang.IllegalArgumentException
import java.time.Instant
import java.util.UUID

Expand Down Expand Up @@ -136,6 +136,29 @@ internal class UserEndpointImplTest {
assertEquals(parentGroup, responseType.parentGroup)
}

@Test
fun `delete a user successfully`() {
val deleteUserDtoCapture = argumentCaptor<DeleteUserRequestDto>()
whenever(lifecycleCoordinator.isRunning).thenReturn(true)
whenever(permissionService.isRunning).thenReturn(true)
whenever(permissionManager.deleteUser(deleteUserDtoCapture.capture())).thenReturn(userResponseDto)

endpoint.start()
val response = endpoint.deleteUser("loginName1")
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 `get a user throws with resource not found exception when the user isn't found`() {
val getUserRequestDtoCapture = argumentCaptor<GetUserRequestDto>()
Expand Down Expand Up @@ -263,7 +286,7 @@ internal class UserEndpointImplTest {
whenever(permissionService.isRunning).thenReturn(true)

endpoint.start()
Assertions.assertThatThrownBy {
assertThatThrownBy {
endpoint.createUser(createUserType.copy(loginName = "foo/bar"))
}.isInstanceOf(InvalidInputDataException::class.java)
.hasMessageContaining("Invalid input data for user creation.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import net.corda.data.permissions.management.PermissionManagementResponse
import net.corda.data.permissions.management.user.AddRoleToUserRequest
import net.corda.data.permissions.management.user.ChangeUserPasswordRequest
import net.corda.data.permissions.management.user.CreateUserRequest
import net.corda.data.permissions.management.user.DeleteUserRequest
import net.corda.data.permissions.management.user.RemoveRoleFromUserRequest
import net.corda.libs.configuration.SmartConfig
import net.corda.libs.permissions.management.cache.PermissionManagementCache
Expand All @@ -15,6 +16,7 @@ import net.corda.libs.permissions.manager.impl.converter.convertToResponseDto
import net.corda.libs.permissions.manager.request.AddRoleToUserRequestDto
import net.corda.libs.permissions.manager.request.ChangeUserPasswordDto
import net.corda.libs.permissions.manager.request.CreateUserRequestDto
import net.corda.libs.permissions.manager.request.DeleteUserRequestDto
import net.corda.libs.permissions.manager.request.GetPermissionSummaryRequestDto
import net.corda.libs.permissions.manager.request.GetUserRequestDto
import net.corda.libs.permissions.manager.request.RemoveRoleFromUserRequestDto
Expand Down Expand Up @@ -78,6 +80,22 @@ class PermissionUserManagerImpl(
return cachedUser.convertToResponseDto()
}

override fun deleteUser(deleteUserRequestDto: DeleteUserRequestDto): UserResponseDto {
val result = sendPermissionWriteRequest<User>(
rpcSender,
writerTimeout,
PermissionManagementRequest(
deleteUserRequestDto.requestedBy,
null,
DeleteUserRequest(
deleteUserRequestDto.loginName
)
)
)

return result.convertToResponseDto()
}

override fun changeUserPasswordSelf(changeUserPasswordDto: ChangeUserPasswordDto): UserResponseDto =
changeUserPassword(changeUserPasswordDto, selfUserPasswordExpiryDays)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import net.corda.data.permissions.management.PermissionManagementRequest
import net.corda.data.permissions.management.PermissionManagementResponse
import net.corda.data.permissions.management.user.AddRoleToUserRequest
import net.corda.data.permissions.management.user.CreateUserRequest
import net.corda.data.permissions.management.user.DeleteUserRequest
import net.corda.data.permissions.management.user.RemoveRoleFromUserRequest
import net.corda.libs.configuration.SmartConfig
import net.corda.libs.configuration.SmartConfigImpl
Expand All @@ -17,6 +18,7 @@ import net.corda.libs.permissions.manager.exception.UnexpectedPermissionResponse
import net.corda.libs.permissions.manager.request.AddRoleToUserRequestDto
import net.corda.libs.permissions.manager.request.ChangeUserPasswordDto
import net.corda.libs.permissions.manager.request.CreateUserRequestDto
import net.corda.libs.permissions.manager.request.DeleteUserRequestDto
import net.corda.libs.permissions.manager.request.GetUserRequestDto
import net.corda.libs.permissions.manager.request.RemoveRoleFromUserRequestDto
import net.corda.libs.permissions.validation.cache.PermissionValidationCache
Expand Down Expand Up @@ -77,6 +79,7 @@ class PermissionUserManagerImplTest {
parentGroup = parentGroup
)

private val deleteUserRequestDto = DeleteUserRequestDto(requestedBy = requestUserName, loginName = "loginname123")
private val userCreationTime = Instant.now()
private val getUserRequestDto = GetUserRequestDto(requestedBy = requestUserName, loginName = "loginname123")
private val changeUserPasswordDto = ChangeUserPasswordDto("requestedBy", "loginname123", "mypassword")
Expand Down Expand Up @@ -213,6 +216,36 @@ class PermissionUserManagerImplTest {
assertThrows(UnexpectedPermissionResponseException::class.java) { manager.createUser(createUserRequestDto) }
}

@Test
fun `delete a user sends rpc request and converts result`() {
val future = mock<CompletableFuture<PermissionManagementResponse>>()
whenever(future.getOrThrow(defaultTimeout)).thenReturn(permissionManagementResponse)

val requestCaptor = argumentCaptor<PermissionManagementRequest>()
whenever(rpcSender.sendRequest(requestCaptor.capture())).thenReturn(future)

val result = manager.deleteUser(deleteUserRequestDto)

val capturedPermissionManagementRequest = requestCaptor.firstValue
assertEquals(requestUserName, capturedPermissionManagementRequest.requestUserId)
assertEquals(null, capturedPermissionManagementRequest.virtualNodeId)

val capturedCreateUserRequest = capturedPermissionManagementRequest.request as DeleteUserRequest
assertEquals(deleteUserRequestDto.loginName, capturedCreateUserRequest.loginName)

assertEquals(fullName, result.fullName)
assertEquals(avroUser.enabled, result.enabled)
assertEquals(avroUser.lastChangeDetails.updateTimestamp, result.lastUpdatedTimestamp)
assertEquals(false, result.ssoAuth)
assertEquals(avroUser.parentGroupId, result.parentGroup)
assertEquals(1, result.properties.size)

val property = result.properties.first()
assertEquals(userProperty.lastChangeDetails.updateTimestamp, property.lastChangedTimestamp)
assertEquals(userProperty.key, property.key)
assertEquals(userProperty.value, property.value)
}

@Test
fun `get a user uses the cache and converts avro user to dto`() {
whenever(permissionManagementCache.getUser("loginname123")).thenReturn(avroUser)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package net.corda.libs.permissions.manager
import net.corda.libs.permissions.manager.request.AddRoleToUserRequestDto
import net.corda.libs.permissions.manager.request.ChangeUserPasswordDto
import net.corda.libs.permissions.manager.request.CreateUserRequestDto
import net.corda.libs.permissions.manager.request.DeleteUserRequestDto
import net.corda.libs.permissions.manager.request.GetPermissionSummaryRequestDto
import net.corda.libs.permissions.manager.request.GetUserRequestDto
import net.corda.libs.permissions.manager.request.RemoveRoleFromUserRequestDto
Expand All @@ -23,6 +24,11 @@ interface PermissionUserManager {
*/
fun getUser(userRequestDto: GetUserRequestDto): UserResponseDto?

/**
* Delete a user in the RBAC Permission System.
*/
fun deleteUser(deleteUserRequestDto: DeleteUserRequestDto): UserResponseDto

/**
* Change a user's own password.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package net.corda.libs.permissions.manager.request

data class DeleteUserRequestDto(
/**
* ID of the user making the request.
*/
val requestedBy: String,
/**
* Login name of the User to delete.
*/
val loginName: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ class PermissionStorageReaderImpl(
publisher.publish(listOf(Record(REST_PERM_USER_TOPIC, key = user.loginName, value = user))).single().getOrThrow()
}

override fun publishDeletedUser(loginName: String) {
publisher.publish(listOf(Record(REST_PERM_USER_TOPIC, key = loginName, value = null))).single().getOrThrow()
}

override fun publishUpdatedRole(role: AvroRole) {
publisher.publish(listOf(Record(REST_PERM_ROLE_TOPIC, key = role.id, value = role))).single().getOrThrow()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ interface PermissionStorageReader : Resource {
*/
fun publishUpdatedUser(user: AvroUser)

/**
* Broadcasts the deleted user onto the messaging bus.
*
* @param loginName The login name of the user to be deleted.
*/
fun publishDeletedUser(loginName: String)

/**
* Broadcasts a new role onto the messaging bus.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import net.corda.data.permissions.management.role.RemovePermissionFromRoleReques
import net.corda.data.permissions.management.user.AddRoleToUserRequest
import net.corda.data.permissions.management.user.ChangeUserPasswordRequest
import net.corda.data.permissions.management.user.CreateUserRequest
import net.corda.data.permissions.management.user.DeleteUserRequest
import net.corda.data.permissions.management.user.RemoveRoleFromUserRequest
import net.corda.libs.permissions.storage.reader.PermissionStorageReader
import net.corda.libs.permissions.storage.writer.PermissionStorageWriterProcessor
Expand Down Expand Up @@ -45,6 +46,12 @@ class PermissionStorageWriterProcessorImpl(
permissionStorageReader.reconcilePermissionSummaries()
avroUser
}
is DeleteUserRequest -> {
val avroUser = userWriter.deleteUser(permissionRequest, request.requestUserId)
permissionStorageReader.publishDeletedUser(avroUser.loginName)
permissionStorageReader.reconcilePermissionSummaries()
avroUser
}
is CreateRoleRequest -> {
val avroRole = roleWriter.createRole(permissionRequest, request.requestUserId)
permissionStorageReader.publishNewRole(avroRole)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,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.ChangeUserPasswordRequest
import net.corda.data.permissions.management.user.CreateUserRequest
import net.corda.data.permissions.management.user.DeleteUserRequest
import net.corda.data.permissions.management.user.RemoveRoleFromUserRequest
import net.corda.data.permissions.User as AvroUser

Expand All @@ -18,6 +19,14 @@ interface UserWriter {
*/
fun createUser(request: CreateUserRequest, requestUserId: String): AvroUser

/**
* Delete a User entity and return its Avro representation.
*
* @param request DeleteUserRequest containing the information of the User to delete.
* @param requestUserId ID of the user who made the request.
*/
fun deleteUser(request: DeleteUserRequest, requestUserId: String): AvroUser

/**
* Change the password field of a User entity and return its Avro representation.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package net.corda.libs.permissions.storage.writer.impl.user.impl
import net.corda.data.permissions.management.user.AddRoleToUserRequest
import net.corda.data.permissions.management.user.ChangeUserPasswordRequest
import net.corda.data.permissions.management.user.CreateUserRequest
import net.corda.data.permissions.management.user.DeleteUserRequest
import net.corda.data.permissions.management.user.RemoveRoleFromUserRequest
import net.corda.libs.permissions.storage.common.converter.toAvroUser
import net.corda.libs.permissions.storage.writer.impl.user.UserWriter
Expand Down Expand Up @@ -36,14 +37,27 @@ class UserWriterImpl(
return entityManagerFactory.transaction { entityManager ->

val validator = EntityValidationUtil(entityManager)
validator.validateUserDoesNotAlreadyExist(request.loginName)
validator.validateUserDoesNotAlreadyExist(loginName)
val parentGroup = validator.validateAndGetOptionalParentGroup(request.parentGroupId)

val user = persistNewUser(request, parentGroup, entityManager, requestUserId, loginName)
user.toAvroUser()
}
}

override fun deleteUser(request: DeleteUserRequest, requestUserId: String): AvroUser {
val loginName = request.loginName
log.debug { "Received request to delete user: $loginName" }
return entityManagerFactory.transaction { entityManager ->

val validator = EntityValidationUtil(entityManager)
val user = validator.validateAndGetUniqueUser(loginName)

val resultUser = removeUser(user, entityManager, requestUserId, loginName)
resultUser.toAvroUser()
}
}

override fun changeUserPassword(
request: ChangeUserPasswordRequest,
requestUserId: String
Expand Down Expand Up @@ -139,6 +153,30 @@ class UserWriterImpl(
return user
}

private fun removeUser(
user: User,
entityManager: EntityManager,
requestUserId: String,
loginName: String
): User {
val updateTimestamp = Instant.now()

entityManager.remove(user)

val auditLog = ChangeAudit(
id = UUID.randomUUID().toString(),
updateTimestamp = updateTimestamp,
actorUser = requestUserId,
changeType = RestPermissionOperation.USER_DELETE,
details = "User '${user.loginName}' deleted by '$requestUserId'."
)

entityManager.persist(auditLog)

log.info("Successfully deleted user: $loginName.")
return user
}

private fun persistUserRoleAssociation(entityManager: EntityManager, requestUserId: String, user: User, role: Role): User {
val updateTimestamp = Instant.now()
val association = RoleUserAssociation(UUID.randomUUID().toString(), role, user, updateTimestamp)
Expand Down
Loading

0 comments on commit 1876501

Please sign in to comment.