Skip to content

Commit

Permalink
CORE-20721: Add RBAC Group rest resource (#6233)
Browse files Browse the repository at this point in the history
This creates the GroupRestResource to allow creation and management of RBAC Groups. It includes all changes to support the Rest worker side of the work, up until sending the PermissionManagementRequest on Kafka. The DB side of this work will be continued in a following piece of work.
  • Loading branch information
Tom-Fitzpatrick authored Jul 1, 2024
1 parent 606aa30 commit ac641f0
Show file tree
Hide file tree
Showing 22 changed files with 1,607 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package net.corda.libs.permissions.endpoints.v1.group.impl

import net.corda.libs.permissions.endpoints.common.PermissionEndpointEventHandler
import net.corda.libs.permissions.endpoints.common.withPermissionManager
import net.corda.libs.permissions.endpoints.v1.converter.convertToDto
import net.corda.libs.permissions.endpoints.v1.converter.convertToEndpointType
import net.corda.libs.permissions.endpoints.v1.group.GroupEndpoint
import net.corda.libs.permissions.endpoints.v1.group.types.CreateGroupType
import net.corda.libs.permissions.endpoints.v1.group.types.GroupContentResponseType
import net.corda.libs.permissions.endpoints.v1.group.types.GroupResponseType
import net.corda.libs.permissions.manager.request.AddRoleToGroupRequestDto
import net.corda.libs.permissions.manager.request.ChangeGroupParentIdDto
import net.corda.libs.permissions.manager.request.DeleteGroupRequestDto
import net.corda.libs.permissions.manager.request.RemoveRoleFromGroupRequestDto
import net.corda.libs.platform.PlatformInfoProvider
import net.corda.lifecycle.Lifecycle
import net.corda.lifecycle.LifecycleCoordinatorFactory
import net.corda.lifecycle.createCoordinator
import net.corda.permissions.management.PermissionManagementService
import net.corda.rest.PluggableRestResource
import net.corda.rest.exception.ExceptionDetails
import net.corda.rest.exception.ResourceNotFoundException
import net.corda.rest.response.ResponseEntity
import net.corda.rest.security.CURRENT_REST_CONTEXT
import org.osgi.service.component.annotations.Activate
import org.osgi.service.component.annotations.Component
import org.osgi.service.component.annotations.Reference
import org.slf4j.Logger
import org.slf4j.LoggerFactory

@Component(service = [PluggableRestResource::class])
class GroupEndpointImpl @Activate constructor(
@Reference(service = LifecycleCoordinatorFactory::class)
private val coordinatorFactory: LifecycleCoordinatorFactory,
@Reference(service = PermissionManagementService::class)
private val permissionManagementService: PermissionManagementService,
@Reference(service = PlatformInfoProvider::class)
private val platformInfoProvider: PlatformInfoProvider
) : GroupEndpoint, PluggableRestResource<GroupEndpoint>, Lifecycle {

private companion object {
val logger: Logger = LoggerFactory.getLogger(this::class.java.enclosingClass)
}

override val targetInterface: Class<GroupEndpoint> = GroupEndpoint::class.java

override val protocolVersion get() = platformInfoProvider.localWorkerPlatformVersion

private val coordinator = coordinatorFactory.createCoordinator<GroupEndpoint>(
PermissionEndpointEventHandler("GroupEndpoint")
)

override fun createGroup(createGroupType: CreateGroupType): ResponseEntity<GroupResponseType> {
val principal = getRestThreadLocalContext()

val createGroupResult = withPermissionManager(permissionManagementService.permissionManager, logger) {
createGroup(createGroupType.convertToDto(principal))
}

return ResponseEntity.created(createGroupResult.convertToEndpointType())
}

override fun changeParentGroup(groupId: String, newParentGroupId: String): ResponseEntity<GroupResponseType> {
val principal = getRestThreadLocalContext()

val groupResponseDto = withPermissionManager(permissionManagementService.permissionManager, logger) {
try {
changeParentGroup(ChangeGroupParentIdDto(principal, groupId, 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(groupResponseDto.convertToEndpointType())
}

override fun addRole(groupId: String, roleId: String): ResponseEntity<GroupResponseType> {
val principal = getRestThreadLocalContext()

val result = withPermissionManager(permissionManagementService.permissionManager, logger) {
addRoleToGroup(AddRoleToGroupRequestDto(principal, groupId, roleId))
}
return ResponseEntity.updated(result.convertToEndpointType())
}

override fun removeRole(groupId: String, roleId: String): ResponseEntity<GroupResponseType> {
val principal = getRestThreadLocalContext()

val result = withPermissionManager(permissionManagementService.permissionManager, logger) {
removeRoleFromGroup(RemoveRoleFromGroupRequestDto(principal, groupId, roleId))
}
return ResponseEntity.updated(result.convertToEndpointType())
}

override fun getGroupContent(groupId: String): GroupContentResponseType {
val groupContentResponseDto = withPermissionManager(permissionManagementService.permissionManager, logger) {
getGroupContent(groupId)
} ?: throw ResourceNotFoundException("Group", groupId)

return groupContentResponseDto.convertToEndpointType()
}

override fun deleteGroup(groupId: String): ResponseEntity<GroupResponseType> {
val principal = getRestThreadLocalContext()

val groupResponseDto = withPermissionManager(permissionManagementService.permissionManager, logger) {
deleteGroup(DeleteGroupRequestDto(principal, groupId))
}

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

private fun getRestThreadLocalContext(): String {
val restContext = CURRENT_REST_CONTEXT.get()
return restContext.principal
}

override val isRunning: Boolean
get() = coordinator.isRunning

override fun start() {
coordinator.start()
}

override fun stop() {
coordinator.stop()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package net.corda.libs.permissions.endpoints.v1.group.impl

import net.corda.libs.permissions.endpoints.v1.group.types.CreateGroupType
import net.corda.libs.permissions.manager.PermissionManager
import net.corda.libs.permissions.manager.request.AddRoleToGroupRequestDto
import net.corda.libs.permissions.manager.request.CreateGroupRequestDto
import net.corda.libs.permissions.manager.request.DeleteGroupRequestDto
import net.corda.libs.permissions.manager.request.RemoveRoleFromGroupRequestDto
import net.corda.libs.permissions.manager.response.GroupContentResponseDto
import net.corda.libs.permissions.manager.response.GroupResponseDto
import net.corda.libs.platform.PlatformInfoProvider
import net.corda.lifecycle.LifecycleCoordinatorFactory
import net.corda.permissions.management.PermissionManagementService
import net.corda.rest.ResponseCode
import net.corda.rest.security.CURRENT_REST_CONTEXT
import net.corda.rest.security.RestAuthContext
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import java.time.Instant
import java.util.UUID

internal class GroupEndpointImplTest {

private val now = Instant.now()
private val parentGroup = UUID.randomUUID().toString()

private val createGroupType = CreateGroupType(
"groupName1",
parentGroup
)

private val groupResponseDto = GroupResponseDto(
"uuid",
now,
0,
"groupName1",
parentGroup,
emptyList(),
emptyList(),
)

private val groupContentResponseDto = GroupContentResponseDto(
"uuid",
now,
"groupName1",
parentGroup,
emptyList(),
emptyList(),
emptySet(),
emptySet()
)

private val permissionManager = mock<PermissionManager>()
private val permissionService = mock<PermissionManagementService>().also {
whenever(it.permissionManager).thenReturn(permissionManager)
}

private val platformInfoProvider = mock<PlatformInfoProvider>().also {
whenever(it.localWorkerPlatformVersion).thenReturn(1)
}

private val lifecycleCoordinatorFactory = mock<LifecycleCoordinatorFactory>()

private val endpoint = GroupEndpointImpl(lifecycleCoordinatorFactory, permissionService, platformInfoProvider)

@BeforeEach
fun beforeEach() {
val authContext = mock<RestAuthContext>().apply {
whenever(principal).thenReturn("aRestUser")
}
CURRENT_REST_CONTEXT.set(authContext)
}

@Test
fun `create a group successfully`() {
val createGroupDtoCapture = argumentCaptor<CreateGroupRequestDto>()
whenever(permissionManager.createGroup(createGroupDtoCapture.capture())).thenReturn(groupResponseDto)

val response = endpoint.createGroup(createGroupType)
val responseType = response.responseBody

assertEquals(ResponseCode.CREATED, response.responseCode)
assertNotNull(responseType)
assertEquals("uuid", responseType.id)
assertEquals(now, responseType.updateTimestamp)
assertEquals("groupName1", responseType.name)
assertEquals(parentGroup, responseType.parentGroupId)

val capturedDto = createGroupDtoCapture.firstValue
assertEquals("groupName1", capturedDto.groupName)
assertEquals(parentGroup, capturedDto.parentGroupId)
}

@Test
fun `get a group successfully`() {
whenever(permissionManager.getGroupContent(any())).thenReturn(groupContentResponseDto)

val groupId = "uuid"
val responseType = endpoint.getGroupContent(groupId)

assertNotNull(responseType)
assertEquals("uuid", responseType.id)
assertEquals(now, responseType.updateTimestamp)
assertEquals("groupName1", responseType.name)
assertEquals(parentGroup, responseType.parentGroupId)
}

@Test
fun `add role to group`() {
val addRoleDtoCapture = argumentCaptor<AddRoleToGroupRequestDto>()
whenever(permissionManager.addRoleToGroup(addRoleDtoCapture.capture())).thenReturn(groupResponseDto)

val groupId = "uuid"
val roleId = "roleId1"
val response = endpoint.addRole(groupId, roleId)
val responseType = response.responseBody

assertEquals(ResponseCode.OK, response.responseCode)
assertNotNull(responseType)
assertEquals("uuid", responseType.id)
assertEquals(now, responseType.updateTimestamp)
assertEquals("groupName1", responseType.name)
assertEquals(parentGroup, responseType.parentGroupId)

val capturedDto = addRoleDtoCapture.firstValue
assertEquals(groupId, capturedDto.groupId)
assertEquals(roleId, capturedDto.roleId)
}

@Test
fun `remove role from group`() {
val removeRoleDtoCapture = argumentCaptor<RemoveRoleFromGroupRequestDto>()
whenever(permissionManager.removeRoleFromGroup(removeRoleDtoCapture.capture())).thenReturn(groupResponseDto)

val groupId = "uuid"
val roleId = "roleId1"
val response = endpoint.removeRole(groupId, roleId)
val responseType = response.responseBody

assertEquals(ResponseCode.OK, response.responseCode)
assertNotNull(responseType)
assertEquals("uuid", responseType.id)
assertEquals(now, responseType.updateTimestamp)
assertEquals("groupName1", responseType.name)
assertEquals(parentGroup, responseType.parentGroupId)

val capturedDto = removeRoleDtoCapture.firstValue
assertEquals(groupId, capturedDto.groupId)
assertEquals(roleId, capturedDto.roleId)
}

@Test
fun `delete a group`() {
val deleteGroupDtoCapture = argumentCaptor<DeleteGroupRequestDto>()
whenever(permissionManager.deleteGroup(deleteGroupDtoCapture.capture())).thenReturn(groupResponseDto)

val groupId = "uuid"
val response = endpoint.deleteGroup(groupId)
val responseType = response.responseBody

assertEquals(ResponseCode.OK, response.responseCode)
assertNotNull(responseType)
assertEquals("uuid", responseType.id)
assertEquals(now, responseType.updateTimestamp)
assertEquals("groupName1", responseType.name)
assertEquals(parentGroup, responseType.parentGroupId)

val capturedDto = deleteGroupDtoCapture.firstValue
assertEquals(groupId, capturedDto.groupId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@Export
package net.corda.libs.permissions.endpoints.v1.group;

import org.osgi.annotation.bundle.Export;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@Export
package net.corda.libs.permissions.endpoints.v1.group.types;

import org.osgi.annotation.bundle.Export;
Loading

0 comments on commit ac641f0

Please sign in to comment.