Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CORE-20731: Implement DeleteUser endpoint #6224

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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