From b311524558311fa996b8856557f2eb282c3fd58a Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Sun, 20 Jun 2021 11:09:43 +0200 Subject: [PATCH 01/28] feat(entity): add endpoints to add / remove rights on entities for an user --- .../authorization/AuthorizationService.kt | 1 + .../Neo4jAuthorizationRepository.kt | 19 ++ .../Neo4jAuthorizationService.kt | 3 + .../StandaloneAuthorizationService.kt | 2 + .../entity/web/EntityAccessControlHandler.kt | 98 ++++++++ .../Neo4jAuthorizationRepositoryTest.kt | 14 ++ .../web/EntityAccessControlHandlerTests.kt | 237 ++++++++++++++++++ .../egm/stellio/shared/util/JsonLdUtils.kt | 1 + 8 files changed, 375 insertions(+) create mode 100644 entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt create mode 100644 entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt index 8e1f314d5..25e11bf22 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt @@ -40,4 +40,5 @@ interface AuthorizationService { fun userIsAdminOfEntity(entityId: URI, userSub: String): Boolean fun createAdminLink(entityId: URI, userSub: String) fun createAdminLinks(entitiesId: List, userSub: String) + fun removeUserRightsOnEntity(entityId: URI, subjectId: URI): Int } diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt index 1237fdb32..c2586c2bb 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt @@ -146,4 +146,23 @@ class Neo4jAuthorizationRepository( .fetch().all() .map { (it["id"] as String).toUri() } } + + fun removeUserRightsOnEntity( + subjectId: URI, + targetId: URI + ): Int { + val matchQuery = + """ + MATCH (subject:Entity { id: ${'$'}entityId })-[:HAS_OBJECT]-(relNode) + -[]->(target:Entity { id: ${'$'}targetId }) + DETACH DELETE relNode + """.trimIndent() + + val parameters = mapOf( + "entityId" to subjectId, + "targetId" to targetId + ) + + return session.query(matchQuery, parameters).queryStatistics().nodesDeleted + } } diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt index 740f3bc4a..a5964bf4b 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt @@ -146,4 +146,7 @@ class Neo4jAuthorizationService( entitiesId ) } + + override fun removeUserRightsOnEntity(entityId: URI, subjectId: URI) = + neo4jAuthorizationRepository.removeUserRightsOnEntity(subjectId, entityId) } diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/StandaloneAuthorizationService.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/StandaloneAuthorizationService.kt index d262cfc57..0e8d152eb 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/StandaloneAuthorizationService.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/StandaloneAuthorizationService.kt @@ -49,4 +49,6 @@ class StandaloneAuthorizationService : AuthorizationService { override fun createAdminLink(entityId: URI, userSub: String) {} override fun createAdminLinks(entitiesId: List, userSub: String) {} + + override fun removeUserRightsOnEntity(entityId: URI, subjectId: URI): Int = 1 } diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt new file mode 100644 index 000000000..92eaf3747 --- /dev/null +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt @@ -0,0 +1,98 @@ +package com.egm.stellio.entity.web + +import com.egm.stellio.entity.authorization.AuthorizationService +import com.egm.stellio.entity.service.EntityService +import com.egm.stellio.shared.model.NgsiLdRelationship +import com.egm.stellio.shared.model.NgsiLdRelationshipInstance +import com.egm.stellio.shared.model.parseToNgsiLdAttributes +import com.egm.stellio.shared.util.JSON_LD_CONTENT_TYPE +import com.egm.stellio.shared.util.JsonLdUtils +import com.egm.stellio.shared.util.checkAndGetContext +import com.egm.stellio.shared.util.toUri +import com.egm.stellio.shared.web.extractSubjectOrEmpty +import kotlinx.coroutines.reactive.awaitFirst +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestHeader +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import reactor.core.publisher.Mono + +@RestController +@RequestMapping("/ngsi-ld/v1/entityAccessControl") +class EntityAccessControlHandler( + private val entityService: EntityService, + private val authorizationService: AuthorizationService +) { + + @PostMapping("/{subjectId}/attrs", consumes = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) + suspend fun addRightsOnEntities( + @RequestHeader httpHeaders: HttpHeaders, + @PathVariable subjectId: String, + @RequestBody requestBody: Mono + ): ResponseEntity<*> { + val userId = extractSubjectOrEmpty().awaitFirst() + val body = requestBody.awaitFirst() + val contexts = checkAndGetContext(httpHeaders, body) + val jsonLdAttributes = JsonLdUtils.expandJsonLdFragment(body, contexts) + val ngsiLdAttributes = parseToNgsiLdAttributes(jsonLdAttributes) + + val (authorizedInstances, unauthorizedInstances) = ngsiLdAttributes + .map { ngsiLdAttribute -> + (ngsiLdAttribute.getAttributeInstances() as List) + .map { Pair(ngsiLdAttribute, it) } + }.flatten() + .partition { + // we don't have any sub-relationships here, so let's just take the first + val targetEntityId = it.second.getLinkedEntitiesIds().first() + authorizationService.userIsAdminOfEntity(targetEntityId, userId) + } + + authorizedInstances.forEach { + entityService.appendEntityRelationship( + subjectId.toUri(), + it.first as NgsiLdRelationship, + it.second, + false + ) + } + + return if (unauthorizedInstances.isEmpty()) + ResponseEntity.status(HttpStatus.NO_CONTENT).build() + else { + val unauthorizedEntities = + unauthorizedInstances.map { it.second.objectId } + .joinToString(",") { "\"$it\"" } + ResponseEntity.status(HttpStatus.MULTI_STATUS).body( + """ + { + "unauthorized entities": [$unauthorizedEntities] + } + """.trimIndent() + ) + } + } + + @DeleteMapping("/{subjectId}/attrs/{entityId}") + suspend fun removeRightsOnEntity( + @RequestHeader httpHeaders: HttpHeaders, + @PathVariable subjectId: String, + @PathVariable entityId: String + ): ResponseEntity<*> { + val userId = extractSubjectOrEmpty().awaitFirst() + + if (!authorizationService.userIsAdminOfEntity(entityId.toUri(), userId)) + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body("User is not authorized to manage rights on entity $entityId") + + authorizationService.removeUserRightsOnEntity(entityId.toUri(), subjectId.toUri()) + + return ResponseEntity.status(HttpStatus.NO_CONTENT).build() + } +} diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt index 5530afe0f..75f89c9e7 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt @@ -469,6 +469,20 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer { assertEquals(2, createdRelations.size) } + @Test + fun `it should remove an user's rights on an entity`() { + val userEntity = createEntity(userUri, listOf(USER_LABEL), mutableListOf()) + val targetEntity = createEntity(apiaryUri, listOf("Apiary"), mutableListOf()) + createRelationship(EntitySubjectNode(userEntity.id), R_CAN_READ, targetEntity.id) + + val result = neo4jAuthorizationRepository.removeUserRightsOnEntity(userEntity.id, targetEntity.id) + + assertEquals(1, result) + + neo4jRepository.deleteEntity(userUri) + neo4jRepository.deleteEntity(apiaryUri) + } + fun createEntity(id: URI, type: List, properties: MutableList): Entity { val entity = Entity(id = id, type = type, properties = properties) return entityRepository.save(entity) diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt new file mode 100644 index 000000000..9e37f0cfb --- /dev/null +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt @@ -0,0 +1,237 @@ +package com.egm.stellio.entity.web + +import com.egm.stellio.entity.authorization.AuthorizationService +import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_READ +import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_WRITE +import com.egm.stellio.entity.config.WebSecurityTestConfig +import com.egm.stellio.entity.service.EntityService +import com.egm.stellio.shared.WithMockCustomUser +import com.egm.stellio.shared.util.JSON_LD_MEDIA_TYPE +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CORE_CONTEXT +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_EGM_AUTHORIZATION_CONTEXT +import com.egm.stellio.shared.util.toUri +import com.ninjasquad.springmockk.MockkBean +import io.mockk.every +import io.mockk.verify +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest +import org.springframework.context.annotation.Import +import org.springframework.http.HttpStatus +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.web.reactive.server.WebTestClient + +@ActiveProfiles("test") +@WebFluxTest(EntityAccessControlHandler::class) +@Import(WebSecurityTestConfig::class) +@WithMockCustomUser(name = "Mock User", username = "mock-user") +class EntityAccessControlHandlerTests { + + @Autowired + private lateinit var webClient: WebTestClient + + @MockkBean(relaxed = true) + private lateinit var entityService: EntityService + + @MockkBean(relaxed = true) + private lateinit var authorizationService: AuthorizationService + + private val subjectId = "urn:ngsi-ld:User:0123".toUri() + private val entityUri1 = "urn:ngsi-ld:Entity:entityId1".toUri() + + @BeforeAll + fun configureWebClientDefaults() { + webClient = webClient.mutate() + .defaultHeaders { + it.accept = listOf(JSON_LD_MEDIA_TYPE) + it.contentType = JSON_LD_MEDIA_TYPE + } + .build() + } + + @Test + fun `it should allow an authorized user to give access to an entity`() { + val requestPayload = + """ + { + "rCanRead": { + "type": "Relationship", + "object": "$entityUri1" + }, + "@context": ["$NGSILD_EGM_AUTHORIZATION_CONTEXT", "$NGSILD_CORE_CONTEXT"] + } + """.trimIndent() + + every { authorizationService.userIsAdminOfEntity(any(), any()) } returns true + + webClient.post() + .uri("/ngsi-ld/v1/entityAccessControl/$subjectId/attrs") + .bodyValue(requestPayload) + .exchange() + .expectStatus().isNoContent + + verify { + authorizationService.userIsAdminOfEntity( + eq(entityUri1), + eq("mock-user") + ) + } + + verify { + entityService.appendEntityRelationship( + eq(subjectId), + match { + it.name == R_CAN_READ && + it.instances.size == 1 + }, + match { it.objectId == entityUri1 }, + eq(false) + ) + } + } + + @Test + fun `it should allow an authorized user to give access to a set of entities`() { + val entityUri2 = "urn:ngsi-ld:Entity:entityId2".toUri() + val entityUri3 = "urn:ngsi-ld:Entity:entityId3".toUri() + val requestPayload = + """ + { + "rCanRead": [{ + "type": "Relationship", + "object": "$entityUri1", + "datasetId": "$entityUri1" + }, + { + "type": "Relationship", + "object": "$entityUri2", + "datasetId": "$entityUri2" + }], + "rCanWrite": { + "type": "Relationship", + "object": "$entityUri3" + }, + "@context": ["$NGSILD_EGM_AUTHORIZATION_CONTEXT", "$NGSILD_CORE_CONTEXT"] + } + """.trimIndent() + + every { authorizationService.userIsAdminOfEntity(any(), any()) } returns true + + webClient.post() + .uri("/ngsi-ld/v1/entityAccessControl/$subjectId/attrs") + .bodyValue(requestPayload) + .exchange() + .expectStatus().isNoContent + + verify { + authorizationService.userIsAdminOfEntity( + eq(entityUri1), + eq("mock-user") + ) + } + + verify { + entityService.appendEntityRelationship( + eq(subjectId), + match { it.name == R_CAN_READ }, + match { it.objectId == entityUri1 }, + eq(false) + ) + entityService.appendEntityRelationship( + eq(subjectId), + match { it.name == R_CAN_READ }, + match { it.objectId == entityUri2 }, + eq(false) + ) + entityService.appendEntityRelationship( + eq(subjectId), + match { it.name == R_CAN_WRITE }, + match { it.objectId == entityUri3 }, + eq(false) + ) + } + } + + @Test + fun `it should only allow to give access to authorized entities`() { + val entityUri2 = "urn:ngsi-ld:Entity:entityId2".toUri() + val requestPayload = + """ + { + "rCanRead": [{ + "type": "Relationship", + "object": "$entityUri1", + "datasetId": "$entityUri1" + }, + { + "type": "Relationship", + "object": "$entityUri2", + "datasetId": "$entityUri2" + }], + "@context": ["$NGSILD_EGM_AUTHORIZATION_CONTEXT", "$NGSILD_CORE_CONTEXT"] + } + """.trimIndent() + + every { authorizationService.userIsAdminOfEntity(eq(entityUri1), any()) } returns true + every { authorizationService.userIsAdminOfEntity(eq(entityUri2), any()) } returns false + + webClient.post() + .uri("/ngsi-ld/v1/entityAccessControl/$subjectId/attrs") + .bodyValue(requestPayload) + .exchange() + .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) + .expectBody().json( + """ + { "unauthorized entities": ["urn:ngsi-ld:Entity:entityId2"]} + """ + ) + + verify(exactly = 1) { + entityService.appendEntityRelationship( + eq(subjectId), + match { it.name == R_CAN_READ }, + match { it.objectId == entityUri1 }, + eq(false) + ) + } + } + + @Test + fun `it should allow an authorized user to remove access to an entity`() { + every { authorizationService.userIsAdminOfEntity(any(), any()) } returns true + + webClient.delete() + .uri("/ngsi-ld/v1/entityAccessControl/$subjectId/attrs/$entityUri1") + .exchange() + .expectStatus().isNoContent + + verify { + authorizationService.userIsAdminOfEntity( + eq(entityUri1), + eq("mock-user") + ) + } + + verify { + authorizationService.removeUserRightsOnEntity(eq(entityUri1), eq(subjectId)) + } + } + + @Test + fun `it should not allow an unauthorized user to remove access to an entity`() { + every { authorizationService.userIsAdminOfEntity(any(), any()) } returns false + + webClient.delete() + .uri("/ngsi-ld/v1/entityAccessControl/$subjectId/attrs/$entityUri1") + .exchange() + .expectStatus().isForbidden + + verify { + authorizationService.userIsAdminOfEntity( + eq(entityUri1), + eq("mock-user") + ) + } + } +} diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt index 9e536dbed..d55adccdf 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt @@ -35,6 +35,7 @@ object JsonLdUtils { const val EGM_BASE_CONTEXT_URL = "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master" val NGSILD_EGM_CONTEXT = "$EGM_BASE_CONTEXT_URL/shared-jsonld-contexts/egm.jsonld" + val NGSILD_EGM_AUTHORIZATION_CONTEXT = "$EGM_BASE_CONTEXT_URL/authorization/jsonld-contexts/authorization.jsonld" val NGSILD_PROPERTY_TYPE = AttributeType("https://uri.etsi.org/ngsi-ld/Property") const val NGSILD_PROPERTY_VALUE = "https://uri.etsi.org/ngsi-ld/hasValue" From 8583ea90569d2ed7d0e1c7f03f3209f4f9b60de1 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Mon, 21 Jun 2021 18:55:53 +0200 Subject: [PATCH 02/28] feat(search): add support for subjects access rights - add model and service to manage subjects access rights - parse of events from cim.iam topic --- .../search/model/SubjectAccessRights.kt | 37 +++ .../egm/stellio/search/service/IAMListener.kt | 73 ++++++ .../service/SubjectAccessRightsService.kt | 127 +++++++++++ .../V0_17__add_user_access_rights_table.sql | 7 + .../search/service/IAMListenerTests.kt | 123 ++++++++++ .../SubjectAccessRightsServiceTests.kt | 210 ++++++++++++++++++ .../GroupMembershipAppendEvent.json | 12 + .../GroupMembershipDeleteEvent.json | 11 + .../authorization/GroupUpdateEvent.json | 11 + .../RealmRoleAppendEventNoRole.json | 8 + .../RealmRoleAppendEventOneRole.json | 8 + .../RealmRoleAppendEventTwoRoles.json | 8 + .../RealmRoleAppendToClient.json | 8 + .../ngsild/authorization/UserCreateEvent.json | 9 + .../ngsild/authorization/UserDeleteEvent.json | 8 + 15 files changed, 660 insertions(+) create mode 100644 search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectAccessRights.kt create mode 100644 search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt create mode 100644 search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt create mode 100644 search-service/src/main/resources/db/migration/V0_17__add_user_access_rights_table.sql create mode 100644 search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt create mode 100644 search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt create mode 100644 search-service/src/test/resources/ngsild/authorization/GroupMembershipAppendEvent.json create mode 100644 search-service/src/test/resources/ngsild/authorization/GroupMembershipDeleteEvent.json create mode 100644 search-service/src/test/resources/ngsild/authorization/GroupUpdateEvent.json create mode 100644 search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventNoRole.json create mode 100644 search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventOneRole.json create mode 100644 search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventTwoRoles.json create mode 100644 search-service/src/test/resources/ngsild/authorization/RealmRoleAppendToClient.json create mode 100644 search-service/src/test/resources/ngsild/authorization/UserCreateEvent.json create mode 100644 search-service/src/test/resources/ngsild/authorization/UserDeleteEvent.json diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectAccessRights.kt b/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectAccessRights.kt new file mode 100644 index 000000000..6667f94c7 --- /dev/null +++ b/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectAccessRights.kt @@ -0,0 +1,37 @@ +package com.egm.stellio.search.model + +import java.net.URI + +data class SubjectAccessRights( + val subjectId: URI, + val subjectType: SubjectType, + val globalRole: String? = null, + val allowedReadEntities: Array? = null, + val allowedWriteEntities: Array? = null +) { + enum class SubjectType { + USER, + GROUP, + CLIENT + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SubjectAccessRights + + if (subjectId != other.subjectId) return false + + return true + } + + override fun hashCode(): Int { + return subjectId.hashCode() + } +} + +fun getSubjectTypeFromSubjectId(subjectId: URI): SubjectAccessRights.SubjectType { + val type = subjectId.toString().split(":")[2] + return SubjectAccessRights.SubjectType.valueOf(type.toUpperCase()) +} diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt new file mode 100644 index 000000000..8609751e4 --- /dev/null +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt @@ -0,0 +1,73 @@ +package com.egm.stellio.search.service + +import com.egm.stellio.search.model.SubjectAccessRights +import com.egm.stellio.search.model.getSubjectTypeFromSubjectId +import com.egm.stellio.shared.model.AttributeAppendEvent +import com.egm.stellio.shared.model.AttributeDeleteEvent +import com.egm.stellio.shared.model.AttributeReplaceEvent +import com.egm.stellio.shared.model.EntityCreateEvent +import com.egm.stellio.shared.model.EntityDeleteEvent +import com.egm.stellio.shared.model.EntityEvent +import com.egm.stellio.shared.util.JsonUtils +import com.fasterxml.jackson.databind.node.ArrayNode +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import org.slf4j.LoggerFactory +import org.springframework.kafka.annotation.KafkaListener +import org.springframework.stereotype.Component + +@Component +class IAMListener( + private val subjectAccessRightsService: SubjectAccessRightsService +) { + + private val logger = LoggerFactory.getLogger(javaClass) + + @KafkaListener(topics = ["cim.iam"], groupId = "search-iam") + fun processMessage(content: String) { + when (val authorizationEvent = JsonUtils.deserializeAs(content)) { + is EntityCreateEvent -> createSubjectAccessRights(authorizationEvent) + is EntityDeleteEvent -> deleteSubjectAccessRights(authorizationEvent) + is AttributeAppendEvent -> addRoleToSubject(authorizationEvent) + is AttributeReplaceEvent -> TODO() + is AttributeDeleteEvent -> TODO() + else -> logger.info("Authorization event ${authorizationEvent.operationType} not handled.") + } + } + + private fun createSubjectAccessRights(entityCreateEvent: EntityCreateEvent) { + val userAccessRights = SubjectAccessRights( + subjectId = entityCreateEvent.entityId, + subjectType = getSubjectTypeFromSubjectId(entityCreateEvent.entityId) + ) + + subjectAccessRightsService.create(userAccessRights) + .subscribe { + logger.debug("Created subject ${entityCreateEvent.entityId}") + } + } + + private fun deleteSubjectAccessRights(entityDeleteEvent: EntityDeleteEvent) { + subjectAccessRightsService.delete(entityDeleteEvent.entityId) + .subscribe { + logger.debug("Deleted subject ${entityDeleteEvent.entityId}") + } + } + + private fun addRoleToSubject(attributeAppendEvent: AttributeAppendEvent) { + if (attributeAppendEvent.attributeName == "roles") { + val operationPayloadNode = jacksonObjectMapper().readTree(attributeAppendEvent.operationPayload) + val updatedRoles = (operationPayloadNode["roles"]["value"] as ArrayNode).elements() + var hasStellioAdminRole = false + while (updatedRoles.hasNext()) { + if (updatedRoles.next().asText().equals("stellio-admin")) { + hasStellioAdminRole = true + break + } + } + if (hasStellioAdminRole) + subjectAccessRightsService.addAdminGlobalRole(attributeAppendEvent.entityId) + else + subjectAccessRightsService.removeAdminGlobalRole(attributeAppendEvent.entityId) + } + } +} diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt new file mode 100644 index 000000000..459035b3b --- /dev/null +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt @@ -0,0 +1,127 @@ +package com.egm.stellio.search.service + +import com.egm.stellio.search.model.SubjectAccessRights +import com.egm.stellio.shared.util.toUri +import org.springframework.data.r2dbc.core.DatabaseClient +import org.springframework.data.r2dbc.core.bind +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import reactor.core.publisher.Mono +import java.net.URI + +@Service +class SubjectAccessRightsService( + private val databaseClient: DatabaseClient +) { + + @Transactional + fun create(subjectAccessRights: SubjectAccessRights): Mono = + databaseClient.execute( + """ + INSERT INTO subject_access_rights + (subject_id, subject_type, global_role, allowed_read_entities, allowed_write_entities) + VALUES (:subject_id, :subject_type, :global_role, :allowed_read_entities, :allowed_write_entities) + """ + ) + .bind("subject_id", subjectAccessRights.subjectId) + .bind("subject_type", subjectAccessRights.subjectType) + .bind("global_role", subjectAccessRights.globalRole) + .bind("allowed_read_entities", subjectAccessRights.allowedReadEntities) + .bind("allowed_write_entities", subjectAccessRights.allowedWriteEntities) + .fetch() + .rowsUpdated() + .thenReturn(1) + .onErrorReturn(-1) + + fun retrieve(subjectId: URI): Mono = + databaseClient.execute( + """ + SELECT * + FROM subject_access_rights + WHERE subject_id = :subject_id + """ + ) + .bind("subject_id", subjectId) + .fetch() + .one() + .map { rowToUserAccessRights(it) } + + @Transactional + fun addReadRoleOnEntity(subjectId: URI, entityId: URI): Mono = + databaseClient.execute( + """ + UPDATE subject_access_rights + SET allowed_read_entities = array_append(allowed_read_entities, :entity_id::text) + WHERE subject_id = :subject_id + """ + ) + .bind("subject_id", subjectId) + .bind("entity_id", entityId) + .fetch() + .rowsUpdated() + .thenReturn(1) + .onErrorReturn(-1) + + fun addAdminGlobalRole(subjectId: URI): Mono = + databaseClient.execute( + """ + UPDATE subject_access_rights + SET global_role = 'stellio-admin' + WHERE subject_id = :subject_id + """ + ) + .bind("subject_id", subjectId) + .fetch() + .rowsUpdated() + .thenReturn(1) + .onErrorReturn(-1) + + fun removeAdminGlobalRole(subjectId: URI): Mono = + databaseClient.execute( + """ + UPDATE subject_access_rights + SET global_role = null + WHERE subject_id = :subject_id + """ + ) + .bind("subject_id", subjectId) + .fetch() + .rowsUpdated() + .thenReturn(1) + .onErrorReturn(-1) + + fun hasReadRoleOnEntity(subjectId: URI, entityId: URI): Mono = + databaseClient.execute( + """ + SELECT COUNT(subject_id) as count + FROM subject_access_rights + WHERE subject_id = :subject_id + AND (:entity_id = ANY(allowed_read_entities) OR :entity_id = ANY(allowed_write_entities)) + """ + ) + .bind("subject_id", subjectId) + .bind("entity_id", entityId) + .fetch() + .one() + .map { + it["count"] as Long == 1L + } + .onErrorReturn(false) + + @Transactional + fun delete(subjectId: URI): Mono = + databaseClient.execute("DELETE FROM subject_access_rights WHERE subject_id = :subject_id") + .bind("subject_id", subjectId) + .fetch() + .rowsUpdated() + .onErrorReturn(-1) + + private fun rowToUserAccessRights(row: Map) = + SubjectAccessRights( + subjectId = (row["subject_id"] as String).toUri(), + subjectType = SubjectAccessRights.SubjectType.valueOf(row["subject_type"] as String), + globalRole = row["global_role"] as String?, + allowedReadEntities = (row["allowed_read_entities"] as Array?), + allowedWriteEntities = (row["allowed_write_entities"] as Array?) + ) +} diff --git a/search-service/src/main/resources/db/migration/V0_17__add_user_access_rights_table.sql b/search-service/src/main/resources/db/migration/V0_17__add_user_access_rights_table.sql new file mode 100644 index 000000000..659370eb7 --- /dev/null +++ b/search-service/src/main/resources/db/migration/V0_17__add_user_access_rights_table.sql @@ -0,0 +1,7 @@ +CREATE TABLE subject_access_rights( + subject_id VARCHAR(64) NOT NULL PRIMARY KEY, + subject_type VARCHAR(64) NOT NULL, + global_role VARCHAR(64), + allowed_read_entities TEXT[], + allowed_write_entities TEXT[] +) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt new file mode 100644 index 000000000..bcd841ead --- /dev/null +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt @@ -0,0 +1,123 @@ +package com.egm.stellio.search.service + +import com.egm.stellio.search.model.SubjectAccessRights +import com.egm.stellio.shared.util.loadSampleData +import com.egm.stellio.shared.util.toUri +import com.ninjasquad.springmockk.MockkBean +import io.mockk.confirmVerified +import io.mockk.verify +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ActiveProfiles + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = [IAMListener::class]) +@ActiveProfiles("test") +class IAMListenerTests { + + @Autowired + private lateinit var iamListener: IAMListener + + @MockkBean(relaxed = true) + private lateinit var subjectAccessRightsService: SubjectAccessRightsService + + @Test + fun `it should handle a create event for a subject`() { + val subjectCreateEvent = loadSampleData("authorization/UserCreateEvent.json") + + iamListener.processMessage(subjectCreateEvent) + + verify { + subjectAccessRightsService.create( + match { + it.subjectId == "urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0".toUri() && + it.subjectType == SubjectAccessRights.SubjectType.USER && + it.globalRole == null && + it.allowedReadEntities.isNullOrEmpty() && + it.allowedWriteEntities.isNullOrEmpty() + } + ) + } + confirmVerified() + } + + @Test + fun `it should handle a delete event for a subject`() { + val subjectDeleteEvent = loadSampleData("authorization/UserDeleteEvent.json") + + iamListener.processMessage(subjectDeleteEvent) + + verify { + subjectAccessRightsService.delete( + match { + it == "urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0".toUri() + } + ) + } + confirmVerified() + } + + @Test + fun `it should handle an append event adding a stellio-admin role for a group`() { + val roleAppendEvent = loadSampleData("authorization/RealmRoleAppendEventOneRole.json") + + iamListener.processMessage(roleAppendEvent) + + verify { + subjectAccessRightsService.addAdminGlobalRole( + match { + it == "urn:ngsi-ld:Group:ab67edf3-238c-4f50-83f4-617c620c62eb".toUri() + } + ) + } + confirmVerified() + } + + @Test + fun `it should handle an append event adding a stellio-admin role for a client`() { + val roleAppendEvent = loadSampleData("authorization/RealmRoleAppendToClient.json") + + iamListener.processMessage(roleAppendEvent) + + verify { + subjectAccessRightsService.addAdminGlobalRole( + match { + it == "urn:ngsi-ld:Client:ab67edf3-238c-4f50-83f4-617c620c62eb".toUri() + } + ) + } + confirmVerified() + } + + @Test + fun `it should handle an append event adding a stellio-admin role within two roles`() { + val roleAppendEvent = loadSampleData("authorization/RealmRoleAppendEventTwoRoles.json") + + iamListener.processMessage(roleAppendEvent) + + verify { + subjectAccessRightsService.addAdminGlobalRole( + match { + it == "urn:ngsi-ld:Group:ab67edf3-238c-4f50-83f4-617c620c62eb".toUri() + } + ) + } + confirmVerified() + } + + @Test + fun `it should handle an append event removing a stellio-admin role for a group`() { + val roleAppendEvent = loadSampleData("authorization/RealmRoleAppendEventNoRole.json") + + iamListener.processMessage(roleAppendEvent) + + verify { + subjectAccessRightsService.removeAdminGlobalRole( + match { + it == "urn:ngsi-ld:Group:ab67edf3-238c-4f50-83f4-617c620c62eb".toUri() + } + ) + } + confirmVerified() + } +} diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt new file mode 100644 index 000000000..bb57b150d --- /dev/null +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt @@ -0,0 +1,210 @@ +package com.egm.stellio.search.service + +import com.egm.stellio.search.config.TimescaleBasedTests +import com.egm.stellio.search.model.SubjectAccessRights +import com.egm.stellio.shared.util.toUri +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.r2dbc.core.DatabaseClient +import org.springframework.test.context.ActiveProfiles +import reactor.test.StepVerifier + +@SpringBootTest +@ActiveProfiles("test") +class SubjectAccessRightsServiceTests : TimescaleBasedTests() { + + @Autowired + private lateinit var subjectAccessRightsService: SubjectAccessRightsService + + @Autowired + private lateinit var databaseClient: DatabaseClient + + private val userUri = "urn:ngsi-ld:User:0123".toUri() + + @AfterEach + fun clearUsersAccessRightsTable() { + databaseClient.delete() + .from("subject_access_rights") + .fetch() + .rowsUpdated() + .block() + } + + @Test + fun `it should persist an user access right`() { + val userAccessRights = SubjectAccessRights( + subjectId = userUri, + subjectType = SubjectAccessRights.SubjectType.USER, + globalRole = "stellio-admin", + allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678"), + allowedWriteEntities = arrayOf("urn:ngsi-ld:Entity:6666", "urn:ngsi-ld:Entity:0000") + ) + + StepVerifier.create( + subjectAccessRightsService.create(userAccessRights) + ) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + } + + @Test + fun `it should retrieve an user access right`() { + val allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678") + val allowedWriteEntities = arrayOf("urn:ngsi-ld:Entity:6666", "urn:ngsi-ld:Entity:0000") + val userAccessRights = SubjectAccessRights( + subjectId = userUri, + subjectType = SubjectAccessRights.SubjectType.USER, + globalRole = "stellio-admin", + allowedReadEntities = allowedReadEntities, + allowedWriteEntities = allowedWriteEntities + ) + + subjectAccessRightsService.create(userAccessRights).block() + + StepVerifier.create( + subjectAccessRightsService.retrieve(userUri) + ) + .expectNextMatches { + it.subjectId == userUri && + it.subjectType == SubjectAccessRights.SubjectType.USER && + it.globalRole == "stellio-admin" && + it.allowedReadEntities?.contentEquals(allowedReadEntities) == true && + it.allowedWriteEntities?.contentEquals(allowedWriteEntities) == true + } + .expectComplete() + .verify() + } + + @Test + fun `it should add a new entity in the allowed list of read entities`() { + val userAccessRights = SubjectAccessRights( + subjectId = userUri, + subjectType = SubjectAccessRights.SubjectType.USER, + globalRole = "stellio-admin", + allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678") + ) + + subjectAccessRightsService.create(userAccessRights).block() + + StepVerifier.create( + subjectAccessRightsService.addReadRoleOnEntity(userUri, "urn:ngsi-ld:Entity:1111".toUri()) + ) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + + StepVerifier.create( + subjectAccessRightsService.hasReadRoleOnEntity(userUri, "urn:ngsi-ld:Entity:1111".toUri()) + ) + .expectNextMatches { it == true } + .expectComplete() + .verify() + } + + @Test + fun `it should update the global role of a subject`() { + val userAccessRights = SubjectAccessRights( + subjectId = userUri, + subjectType = SubjectAccessRights.SubjectType.USER, + allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678"), + allowedWriteEntities = arrayOf("urn:ngsi-ld:Entity:6666") + ) + + subjectAccessRightsService.create(userAccessRights).block() + + StepVerifier.create( + subjectAccessRightsService.addAdminGlobalRole(userUri) + ) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + + StepVerifier.create( + subjectAccessRightsService.retrieve(userUri) + ) + .expectNextMatches { + it.globalRole == "stellio-admin" + } + .expectComplete() + .verify() + + StepVerifier.create( + subjectAccessRightsService.removeAdminGlobalRole(userUri) + ) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + + StepVerifier.create( + subjectAccessRightsService.retrieve(userUri) + ) + .expectNextMatches { + it.globalRole == null + } + .expectComplete() + .verify() + } + + @Test + fun `it should find if an user has a read role on a entity`() { + val userAccessRights = SubjectAccessRights( + subjectId = userUri, + subjectType = SubjectAccessRights.SubjectType.USER, + globalRole = "stellio-admin", + allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678"), + allowedWriteEntities = arrayOf("urn:ngsi-ld:Entity:6666") + ) + + subjectAccessRightsService.create(userAccessRights).block() + + StepVerifier.create( + subjectAccessRightsService.hasReadRoleOnEntity(userUri, "urn:ngsi-ld:Entity:1234".toUri()) + ) + .expectNextMatches { it == true } + .expectComplete() + .verify() + + StepVerifier.create( + subjectAccessRightsService.hasReadRoleOnEntity(userUri, "urn:ngsi-ld:Entity:1111".toUri()) + ) + .expectNextMatches { it == false } + .expectComplete() + .verify() + + StepVerifier.create( + subjectAccessRightsService.hasReadRoleOnEntity(userUri, "urn:ngsi-ld:Entity:6666".toUri()) + ) + .expectNextMatches { it == true } + .expectComplete() + .verify() + } + + @Test + fun `it should delete an user access right`() { + val userAccessRights = SubjectAccessRights( + subjectId = userUri, + subjectType = SubjectAccessRights.SubjectType.USER, + globalRole = "stellio-admin", + allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678"), + allowedWriteEntities = arrayOf("urn:ngsi-ld:Entity:6666", "urn:ngsi-ld:Entity:0000") + ) + + subjectAccessRightsService.create(userAccessRights).block() + + StepVerifier.create( + subjectAccessRightsService.delete(userUri) + ) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + + StepVerifier.create( + subjectAccessRightsService.retrieve(userUri) + ) + .expectComplete() + .verify() + } +} diff --git a/search-service/src/test/resources/ngsild/authorization/GroupMembershipAppendEvent.json b/search-service/src/test/resources/ngsild/authorization/GroupMembershipAppendEvent.json new file mode 100644 index 000000000..e934fad19 --- /dev/null +++ b/search-service/src/test/resources/ngsild/authorization/GroupMembershipAppendEvent.json @@ -0,0 +1,12 @@ +{ + "operationType": "ATTRIBUTE_APPEND", + "entityId": "urn:ngsi-ld:User:96e1f1e9-d798-48d7-820e-59f5a9a2abf5", + "attributeName": "isMemberOf", + "datasetId": "urn:ngsi-ld:Dataset:7cdad168-96ee-4649-b768-a060ac2ef435", + "operationPayload": "{\"isMemberOf\":[{\"type\":\"Relationship\",\"datasetId\":\"urn:ngsi-ld:Dataset:7cdad168-96ee-4649-b768-a060ac2ef435\",\"object\":\"urn:ngsi-ld:Group:7cdad168-96ee-4649-b768-a060ac2ef435\"}]}", + "updatedEntity": "", + "contexts": [ + "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld", + "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" + ] +} diff --git a/search-service/src/test/resources/ngsild/authorization/GroupMembershipDeleteEvent.json b/search-service/src/test/resources/ngsild/authorization/GroupMembershipDeleteEvent.json new file mode 100644 index 000000000..ad8cf1cd9 --- /dev/null +++ b/search-service/src/test/resources/ngsild/authorization/GroupMembershipDeleteEvent.json @@ -0,0 +1,11 @@ +{ + "operationType": "ATTRIBUTE_DELETE", + "entityId": "urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0", + "attributeName": "isMemberOf", + "datasetId": "urn:ngsi-ld:Dataset:isMemberOf:7cdad168-96ee-4649-b768-a060ac2ef435", + "updatedEntity": "", + "contexts": [ + "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld", + "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" + ] +} diff --git a/search-service/src/test/resources/ngsild/authorization/GroupUpdateEvent.json b/search-service/src/test/resources/ngsild/authorization/GroupUpdateEvent.json new file mode 100644 index 000000000..d340d3d27 --- /dev/null +++ b/search-service/src/test/resources/ngsild/authorization/GroupUpdateEvent.json @@ -0,0 +1,11 @@ +{ + "operationType": "ATTRIBUTE_REPLACE", + "entityId": "urn:ngsi-ld:Group:ab67edf3-238c-4f50-83f4-617c620c62eb", + "attributeName": "name", + "operationPayload": "{\"name\":{\"type\":\"Property\",\"value\":\"EGM Team\"}}", + "updatedEntity": "", + "contexts": [ + "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld", + "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" + ] +} diff --git a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventNoRole.json b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventNoRole.json new file mode 100644 index 000000000..ab4695b75 --- /dev/null +++ b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventNoRole.json @@ -0,0 +1,8 @@ +{ + "operationType":"ATTRIBUTE_APPEND", + "entityId":"urn:ngsi-ld:Group:ab67edf3-238c-4f50-83f4-617c620c62eb", + "attributeName": "roles", + "operationPayload":"{\"roles\":{\"type\":\"Property\",\"value\":[]}}", + "updatedEntity": "", + "contexts":["https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld","http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld"] +} diff --git a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventOneRole.json b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventOneRole.json new file mode 100644 index 000000000..df12ab5f5 --- /dev/null +++ b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventOneRole.json @@ -0,0 +1,8 @@ +{ + "operationType":"ATTRIBUTE_APPEND", + "entityId":"urn:ngsi-ld:Group:ab67edf3-238c-4f50-83f4-617c620c62eb", + "attributeName": "roles", + "operationPayload":"{\"roles\":{\"type\":\"Property\",\"value\":[\"stellio-admin\"]}}", + "updatedEntity": "", + "contexts":["https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld","http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld"] +} diff --git a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventTwoRoles.json b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventTwoRoles.json new file mode 100644 index 000000000..f1244eeae --- /dev/null +++ b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventTwoRoles.json @@ -0,0 +1,8 @@ +{ + "operationType":"ATTRIBUTE_APPEND", + "entityId":"urn:ngsi-ld:Group:ab67edf3-238c-4f50-83f4-617c620c62eb", + "attributeName": "roles", + "operationPayload":"{\"roles\":{\"type\":\"Property\",\"value\":[\"stellio-admin\", \"stellio-creator\"]}}", + "updatedEntity": "", + "contexts":["https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld","http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld"] +} diff --git a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendToClient.json b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendToClient.json new file mode 100644 index 000000000..6d5080eeb --- /dev/null +++ b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendToClient.json @@ -0,0 +1,8 @@ +{ + "operationType":"ATTRIBUTE_APPEND", + "entityId":"urn:ngsi-ld:Client:ab67edf3-238c-4f50-83f4-617c620c62eb", + "attributeName": "roles", + "operationPayload":"{\"serviceAccountId\":{\"type\":\"Property\",\"value\":\"a1eec755-c455-4f28-abe4-06558176fcc5\"},\"roles\":{\"type\":\"Property\",\"value\":[\"stellio-admin\"]}}", + "updatedEntity": "", + "contexts":["https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld","http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld"] +} diff --git a/search-service/src/test/resources/ngsild/authorization/UserCreateEvent.json b/search-service/src/test/resources/ngsild/authorization/UserCreateEvent.json new file mode 100644 index 000000000..e718e197f --- /dev/null +++ b/search-service/src/test/resources/ngsild/authorization/UserCreateEvent.json @@ -0,0 +1,9 @@ +{ + "operationType": "ENTITY_CREATE", + "entityId": "urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0", + "operationPayload": "{\"id\":\"urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0\",\"type\":\"User\",\"username\":{\"type\":\"Property\",\"value\":\"stellio\"}}", + "contexts": [ + "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld", + "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" + ] +} diff --git a/search-service/src/test/resources/ngsild/authorization/UserDeleteEvent.json b/search-service/src/test/resources/ngsild/authorization/UserDeleteEvent.json new file mode 100644 index 000000000..d8e1ffcc4 --- /dev/null +++ b/search-service/src/test/resources/ngsild/authorization/UserDeleteEvent.json @@ -0,0 +1,8 @@ +{ + "operationType": "ENTITY_DELETE", + "entityId": "urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0", + "contexts": [ + "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld", + "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" + ] +} From fa2306c6ca31e642508059efdeea9c0f1809b441 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Wed, 23 Jun 2021 08:39:02 +0200 Subject: [PATCH 03/28] feat(entity): propagate changes of access rights on entities to Kafka --- .../entity/web/EntityAccessControlHandler.kt | 38 ++++++++++++++- .../web/EntityAccessControlHandlerTests.kt | 47 +++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt index 92eaf3747..af0cc7e57 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt @@ -2,12 +2,16 @@ package com.egm.stellio.entity.web import com.egm.stellio.entity.authorization.AuthorizationService import com.egm.stellio.entity.service.EntityService +import com.egm.stellio.shared.model.AttributeAppendEvent +import com.egm.stellio.shared.model.AttributeDeleteEvent import com.egm.stellio.shared.model.NgsiLdRelationship import com.egm.stellio.shared.model.NgsiLdRelationshipInstance import com.egm.stellio.shared.model.parseToNgsiLdAttributes import com.egm.stellio.shared.util.JSON_LD_CONTENT_TYPE import com.egm.stellio.shared.util.JsonLdUtils +import com.egm.stellio.shared.util.JsonUtils import com.egm.stellio.shared.util.checkAndGetContext +import com.egm.stellio.shared.util.getContextFromLinkHeaderOrDefault import com.egm.stellio.shared.util.toUri import com.egm.stellio.shared.web.extractSubjectOrEmpty import kotlinx.coroutines.reactive.awaitFirst @@ -15,6 +19,7 @@ import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity +import org.springframework.kafka.core.KafkaTemplate import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping @@ -28,7 +33,8 @@ import reactor.core.publisher.Mono @RequestMapping("/ngsi-ld/v1/entityAccessControl") class EntityAccessControlHandler( private val entityService: EntityService, - private val authorizationService: AuthorizationService + private val authorizationService: AuthorizationService, + private val kafkaTemplate: KafkaTemplate ) { @PostMapping("/{subjectId}/attrs", consumes = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) @@ -55,12 +61,30 @@ class EntityAccessControlHandler( } authorizedInstances.forEach { + val ngsiLdRelationship = it.first as NgsiLdRelationship entityService.appendEntityRelationship( subjectId.toUri(), - it.first as NgsiLdRelationship, + ngsiLdRelationship, it.second, false ) + + val operationPayload = mapOf( + ngsiLdRelationship.compactName to mapOf( + "type" to "Relationship", + "object" to it.second.objectId, + "datasetId" to it.second.datasetId + ) + ) + val attributeAppendEvent = AttributeAppendEvent( + entityId = subjectId.toUri(), + attributeName = ngsiLdRelationship.compactName, + datasetId = it.second.datasetId, + operationPayload = JsonUtils.serializeObject(operationPayload), + updatedEntity = "", + contexts = contexts + ) + kafkaTemplate.send("cim.iam.rights", subjectId, JsonUtils.serializeObject(attributeAppendEvent)) } return if (unauthorizedInstances.isEmpty()) @@ -86,6 +110,7 @@ class EntityAccessControlHandler( @PathVariable entityId: String ): ResponseEntity<*> { val userId = extractSubjectOrEmpty().awaitFirst() + val contexts = listOf(getContextFromLinkHeaderOrDefault(httpHeaders)) if (!authorizationService.userIsAdminOfEntity(entityId.toUri(), userId)) return ResponseEntity.status(HttpStatus.FORBIDDEN) @@ -93,6 +118,15 @@ class EntityAccessControlHandler( authorizationService.removeUserRightsOnEntity(entityId.toUri(), subjectId.toUri()) + val attributeAppendEvent = AttributeDeleteEvent( + entityId = subjectId.toUri(), + attributeName = entityId, + datasetId = null, + updatedEntity = "", + contexts = contexts + ) + kafkaTemplate.send("cim.iam.rights", subjectId, JsonUtils.serializeObject(attributeAppendEvent)) + return ResponseEntity.status(HttpStatus.NO_CONTENT).build() } } diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt index 9e37f0cfb..f37fd2350 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt @@ -6,9 +6,15 @@ import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN import com.egm.stellio.entity.config.WebSecurityTestConfig import com.egm.stellio.entity.service.EntityService import com.egm.stellio.shared.WithMockCustomUser +import com.egm.stellio.shared.model.AttributeAppendEvent +import com.egm.stellio.shared.model.AttributeDeleteEvent +import com.egm.stellio.shared.model.EventsType import com.egm.stellio.shared.util.JSON_LD_MEDIA_TYPE import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CORE_CONTEXT import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_EGM_AUTHORIZATION_CONTEXT +import com.egm.stellio.shared.util.JsonUtils +import com.egm.stellio.shared.util.buildContextLinkHeader +import com.egm.stellio.shared.util.matchContent import com.egm.stellio.shared.util.toUri import com.ninjasquad.springmockk.MockkBean import io.mockk.every @@ -18,9 +24,12 @@ import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest import org.springframework.context.annotation.Import +import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus +import org.springframework.kafka.core.KafkaTemplate import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.util.concurrent.SettableListenableFuture @ActiveProfiles("test") @WebFluxTest(EntityAccessControlHandler::class) @@ -37,6 +46,9 @@ class EntityAccessControlHandlerTests { @MockkBean(relaxed = true) private lateinit var authorizationService: AuthorizationService + @MockkBean + private lateinit var kafkaTemplate: KafkaTemplate + private val subjectId = "urn:ngsi-ld:User:0123".toUri() private val entityUri1 = "urn:ngsi-ld:Entity:entityId1".toUri() @@ -64,6 +76,7 @@ class EntityAccessControlHandlerTests { """.trimIndent() every { authorizationService.userIsAdminOfEntity(any(), any()) } returns true + every { kafkaTemplate.send(any(), any(), any()) } returns SettableListenableFuture() webClient.post() .uri("/ngsi-ld/v1/entityAccessControl/$subjectId/attrs") @@ -89,6 +102,24 @@ class EntityAccessControlHandlerTests { eq(false) ) } + + verify { + kafkaTemplate.send( + eq("cim.iam.rights"), + eq(subjectId.toString()), + match { + val event = JsonUtils.deserializeAs(it) + val expectedOperationPayload = + "{\"rCanRead\":{\"type\":\"Relationship\",\"object\":\"urn:ngsi-ld:Entity:entityId1\"}}" + event.entityId == subjectId && + event.attributeName == "rCanRead" && + event.operationType == EventsType.ATTRIBUTE_APPEND && + event.contexts.size == 2 && + event.datasetId == null && + event.operationPayload.matchContent(expectedOperationPayload) + } + ) + } } @Test @@ -200,9 +231,11 @@ class EntityAccessControlHandlerTests { @Test fun `it should allow an authorized user to remove access to an entity`() { every { authorizationService.userIsAdminOfEntity(any(), any()) } returns true + every { kafkaTemplate.send(any(), any(), any()) } returns SettableListenableFuture() webClient.delete() .uri("/ngsi-ld/v1/entityAccessControl/$subjectId/attrs/$entityUri1") + .header(HttpHeaders.LINK, buildContextLinkHeader(NGSILD_EGM_AUTHORIZATION_CONTEXT)) .exchange() .expectStatus().isNoContent @@ -216,6 +249,20 @@ class EntityAccessControlHandlerTests { verify { authorizationService.removeUserRightsOnEntity(eq(entityUri1), eq(subjectId)) } + + verify { + kafkaTemplate.send( + eq("cim.iam.rights"), + eq(subjectId.toString()), + match { + val event = JsonUtils.deserializeAs(it) + event.entityId == subjectId && + event.attributeName == entityUri1.toString() && + event.operationType == EventsType.ATTRIBUTE_DELETE && + event.contexts.size == 1 + } + ) + } } @Test From c3eb55b291263517f412dee491d6dadefe599986 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Wed, 23 Jun 2021 09:35:52 +0200 Subject: [PATCH 04/28] feat(entity): handle case where the right to be removed does not exist --- .../entity/web/EntityAccessControlHandler.kt | 26 ++++++++++++------- .../web/EntityAccessControlHandlerTests.kt | 21 +++++++++++++++ 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt index af0cc7e57..dcf6c057f 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt @@ -6,6 +6,7 @@ import com.egm.stellio.shared.model.AttributeAppendEvent import com.egm.stellio.shared.model.AttributeDeleteEvent import com.egm.stellio.shared.model.NgsiLdRelationship import com.egm.stellio.shared.model.NgsiLdRelationshipInstance +import com.egm.stellio.shared.model.ResourceNotFoundException import com.egm.stellio.shared.model.parseToNgsiLdAttributes import com.egm.stellio.shared.util.JSON_LD_CONTENT_TYPE import com.egm.stellio.shared.util.JsonLdUtils @@ -116,17 +117,22 @@ class EntityAccessControlHandler( return ResponseEntity.status(HttpStatus.FORBIDDEN) .body("User is not authorized to manage rights on entity $entityId") - authorizationService.removeUserRightsOnEntity(entityId.toUri(), subjectId.toUri()) + val removeResult = authorizationService.removeUserRightsOnEntity(entityId.toUri(), subjectId.toUri()) - val attributeAppendEvent = AttributeDeleteEvent( - entityId = subjectId.toUri(), - attributeName = entityId, - datasetId = null, - updatedEntity = "", - contexts = contexts - ) - kafkaTemplate.send("cim.iam.rights", subjectId, JsonUtils.serializeObject(attributeAppendEvent)) + if (removeResult == 1) { + val attributeAppendEvent = AttributeDeleteEvent( + entityId = subjectId.toUri(), + attributeName = entityId, + datasetId = null, + updatedEntity = "", + contexts = contexts + ) + kafkaTemplate.send("cim.iam.rights", subjectId, JsonUtils.serializeObject(attributeAppendEvent)) + } - return ResponseEntity.status(HttpStatus.NO_CONTENT).build() + return if (removeResult == 1) + ResponseEntity.status(HttpStatus.NO_CONTENT).build() + else + throw ResourceNotFoundException("Subject $subjectId has no right on entity $entityId") } } diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt index f37fd2350..b20c5c04e 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt @@ -231,6 +231,7 @@ class EntityAccessControlHandlerTests { @Test fun `it should allow an authorized user to remove access to an entity`() { every { authorizationService.userIsAdminOfEntity(any(), any()) } returns true + every { authorizationService.removeUserRightsOnEntity(any(), any()) } returns 1 every { kafkaTemplate.send(any(), any(), any()) } returns SettableListenableFuture() webClient.delete() @@ -281,4 +282,24 @@ class EntityAccessControlHandlerTests { ) } } + + @Test + fun `it should return a 404 if the subject has no right on the target entity`() { + every { authorizationService.userIsAdminOfEntity(any(), any()) } returns true + every { authorizationService.removeUserRightsOnEntity(any(), any()) } returns 0 + + webClient.delete() + .uri("/ngsi-ld/v1/entityAccessControl/$subjectId/attrs/$entityUri1") + .exchange() + .expectStatus().isNotFound + .expectBody().json( + """ + { + "detail": "Subject urn:ngsi-ld:User:0123 has no right on entity urn:ngsi-ld:Entity:entityId1", + "type": "https://uri.etsi.org/ngsi-ld/errors/ResourceNotFound", + "title": "The referred resource has not been found" + } + """.trimIndent() + ) + } } From c64ae2f764d79b3ce2fa6d49bce69ded6dba76a2 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Wed, 23 Jun 2021 10:53:00 +0200 Subject: [PATCH 05/28] fix(entity): remove attribute name key in operation payloads --- .../egm/stellio/entity/web/EntityAccessControlHandler.kt | 8 +++----- .../stellio/entity/web/EntityAccessControlHandlerTests.kt | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt index dcf6c057f..7fd9774b5 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt @@ -71,11 +71,9 @@ class EntityAccessControlHandler( ) val operationPayload = mapOf( - ngsiLdRelationship.compactName to mapOf( - "type" to "Relationship", - "object" to it.second.objectId, - "datasetId" to it.second.datasetId - ) + "type" to "Relationship", + "object" to it.second.objectId, + "datasetId" to it.second.datasetId ) val attributeAppendEvent = AttributeAppendEvent( entityId = subjectId.toUri(), diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt index b20c5c04e..a09ce9dcf 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt @@ -46,7 +46,7 @@ class EntityAccessControlHandlerTests { @MockkBean(relaxed = true) private lateinit var authorizationService: AuthorizationService - @MockkBean + @MockkBean(relaxed = true) private lateinit var kafkaTemplate: KafkaTemplate private val subjectId = "urn:ngsi-ld:User:0123".toUri() @@ -110,7 +110,7 @@ class EntityAccessControlHandlerTests { match { val event = JsonUtils.deserializeAs(it) val expectedOperationPayload = - "{\"rCanRead\":{\"type\":\"Relationship\",\"object\":\"urn:ngsi-ld:Entity:entityId1\"}}" + "{\"type\":\"Relationship\",\"object\":\"urn:ngsi-ld:Entity:entityId1\"}" event.entityId == subjectId && event.attributeName == "rCanRead" && event.operationType == EventsType.ATTRIBUTE_APPEND && From 8bfb10e2171bebe174d1356556c23fff24b942c5 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Wed, 23 Jun 2021 10:54:37 +0200 Subject: [PATCH 06/28] feat(search): add listener for events related to access rights on entities --- .../egm/stellio/search/service/IAMListener.kt | 31 +++++++++++++++++ .../service/SubjectAccessRightsService.kt | 33 +++++++++++++++++++ .../search/service/IAMListenerTests.kt | 30 +++++++++++++++++ .../SubjectAccessRightsServiceTests.kt | 26 +++++++++++++++ .../authorization/RightAddOnEntity.json | 11 +++++++ .../authorization/RightRemoveOnEntity.json | 9 +++++ 6 files changed, 140 insertions(+) create mode 100644 search-service/src/test/resources/ngsild/authorization/RightAddOnEntity.json create mode 100644 search-service/src/test/resources/ngsild/authorization/RightRemoveOnEntity.json diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt index 8609751e4..34ce7149e 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt @@ -9,6 +9,7 @@ import com.egm.stellio.shared.model.EntityCreateEvent import com.egm.stellio.shared.model.EntityDeleteEvent import com.egm.stellio.shared.model.EntityEvent import com.egm.stellio.shared.util.JsonUtils +import com.egm.stellio.shared.util.toUri import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.slf4j.LoggerFactory @@ -34,6 +35,15 @@ class IAMListener( } } + @KafkaListener(topics = ["cim.iam.rights"], groupId = "search-iam-rights") + fun processIamRights(content: String) { + when (val authorizationEvent = JsonUtils.deserializeAs(content)) { + is AttributeAppendEvent -> addEntityToSubject(authorizationEvent) + is AttributeDeleteEvent -> removeEntityFromSubject(authorizationEvent) + else -> logger.info("Authorization event ${authorizationEvent.operationType} not handled.") + } + } + private fun createSubjectAccessRights(entityCreateEvent: EntityCreateEvent) { val userAccessRights = SubjectAccessRights( subjectId = entityCreateEvent.entityId, @@ -70,4 +80,25 @@ class IAMListener( subjectAccessRightsService.removeAdminGlobalRole(attributeAppendEvent.entityId) } } + + private fun addEntityToSubject(attributeAppendEvent: AttributeAppendEvent) { + val operationPayloadNode = jacksonObjectMapper().readTree(attributeAppendEvent.operationPayload) + val entityId = operationPayloadNode["object"].asText() + when (attributeAppendEvent.attributeName) { + "rCanRead" -> { + subjectAccessRightsService.addReadRoleOnEntity(attributeAppendEvent.entityId, entityId.toUri()) + } + "rCanWrite" -> { + subjectAccessRightsService.addWriteRoleOnEntity(attributeAppendEvent.entityId, entityId.toUri()) + } + else -> logger.warn("Unrecognized attribute name ${attributeAppendEvent.attributeName}") + } + } + + private fun removeEntityFromSubject(attributeDeleteEvent: AttributeDeleteEvent) { + subjectAccessRightsService.removeRoleOnEntity( + attributeDeleteEvent.entityId, + attributeDeleteEvent.attributeName.toUri() + ) + } } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt index 459035b3b..eacdb064c 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt @@ -62,6 +62,39 @@ class SubjectAccessRightsService( .thenReturn(1) .onErrorReturn(-1) + @Transactional + fun addWriteRoleOnEntity(subjectId: URI, entityId: URI): Mono = + databaseClient.execute( + """ + UPDATE subject_access_rights + SET allowed_write_entities = array_append(allowed_write_entities, :entity_id::text) + WHERE subject_id = :subject_id + """ + ) + .bind("subject_id", subjectId) + .bind("entity_id", entityId) + .fetch() + .rowsUpdated() + .thenReturn(1) + .onErrorReturn(-1) + + @Transactional + fun removeRoleOnEntity(subjectId: URI, entityId: URI): Mono = + databaseClient.execute( + """ + UPDATE subject_access_rights + SET allowed_read_entities = array_remove(allowed_read_entities, :entity_id::text), + allowed_write_entities = array_remove(allowed_write_entities, :entity_id::text) + WHERE subject_id = :subject_id + """ + ) + .bind("subject_id", subjectId) + .bind("entity_id", entityId) + .fetch() + .rowsUpdated() + .thenReturn(1) + .onErrorReturn(-1) + fun addAdminGlobalRole(subjectId: URI): Mono = databaseClient.execute( """ diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt index bcd841ead..b034f596a 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt @@ -120,4 +120,34 @@ class IAMListenerTests { } confirmVerified() } + + @Test + fun `it should handle an append event adding a right on an entity`() { + val rightAppendEvent = loadSampleData("authorization/RightAddOnEntity.json") + + iamListener.processIamRights(rightAppendEvent) + + verify { + subjectAccessRightsService.addReadRoleOnEntity( + eq("urn:ngsi-ld:User:312b30b4-9279-4f7e-bdc5-ec56d699bb7d".toUri()), + eq("urn:ngsi-ld:Beekeeper:01".toUri()) + ) + } + confirmVerified() + } + + @Test + fun `it should handle an delete event removing a right on an entity`() { + val rightRemoveEvent = loadSampleData("authorization/RightRemoveOnEntity.json") + + iamListener.processIamRights(rightRemoveEvent) + + verify { + subjectAccessRightsService.removeRoleOnEntity( + eq("urn:ngsi-ld:User:312b30b4-9279-4f7e-bdc5-ec56d699bb7d".toUri()), + eq("urn:ngsi-ld:Beekeeper:01".toUri()) + ) + } + confirmVerified() + } } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt index bb57b150d..399c64332 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt @@ -104,6 +104,32 @@ class SubjectAccessRightsServiceTests : TimescaleBasedTests() { .verify() } + @Test + fun `it should remove an entity from the allowed list of read entities`() { + val userAccessRights = SubjectAccessRights( + subjectId = userUri, + subjectType = SubjectAccessRights.SubjectType.USER, + globalRole = "stellio-admin", + allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678") + ) + + subjectAccessRightsService.create(userAccessRights).block() + + StepVerifier.create( + subjectAccessRightsService.removeRoleOnEntity(userUri, "urn:ngsi-ld:Entity:1234".toUri()) + ) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + + StepVerifier.create( + subjectAccessRightsService.hasReadRoleOnEntity(userUri, "urn:ngsi-ld:Entity:1234".toUri()) + ) + .expectNextMatches { it == false } + .expectComplete() + .verify() + } + @Test fun `it should update the global role of a subject`() { val userAccessRights = SubjectAccessRights( diff --git a/search-service/src/test/resources/ngsild/authorization/RightAddOnEntity.json b/search-service/src/test/resources/ngsild/authorization/RightAddOnEntity.json new file mode 100644 index 000000000..dd24aedab --- /dev/null +++ b/search-service/src/test/resources/ngsild/authorization/RightAddOnEntity.json @@ -0,0 +1,11 @@ +{ + "entityId": "urn:ngsi-ld:User:312b30b4-9279-4f7e-bdc5-ec56d699bb7d", + "attributeName": "rCanRead", + "operationPayload": "{\"type\":\"Relationship\",\"object\":\"urn:ngsi-ld:Beekeeper:01\"}", + "updatedEntity": "", + "contexts": [ + "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/apic/jsonld-contexts/apic-compound.jsonld" + ], + "overwrite": true, + "operationType": "ATTRIBUTE_APPEND" +} diff --git a/search-service/src/test/resources/ngsild/authorization/RightRemoveOnEntity.json b/search-service/src/test/resources/ngsild/authorization/RightRemoveOnEntity.json new file mode 100644 index 000000000..da0265e34 --- /dev/null +++ b/search-service/src/test/resources/ngsild/authorization/RightRemoveOnEntity.json @@ -0,0 +1,9 @@ +{ + "entityId": "urn:ngsi-ld:User:312b30b4-9279-4f7e-bdc5-ec56d699bb7d", + "attributeName": "urn:ngsi-ld:Beekeeper:01", + "updatedEntity": "", + "contexts": [ + "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/apic/jsonld-contexts/apic-compound.jsonld" + ], + "operationType": "ATTRIBUTE_DELETE" +} From 7b211a684925c8f5aaf1ae7513984ac3eecda29e Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Wed, 23 Jun 2021 22:29:04 +0200 Subject: [PATCH 07/28] refactor(search): store sub uuid instead of entity URI - this is the information we get from the JWT --- .../search/model/SubjectAccessRights.kt | 16 +---- .../egm/stellio/search/service/IAMListener.kt | 23 ++++--- .../service/SubjectAccessRightsService.kt | 25 ++++---- .../V0_17__add_user_access_rights_table.sql | 2 +- .../search/service/IAMListenerTests.kt | 21 +++--- .../SubjectAccessRightsServiceTests.kt | 64 ++++++++++--------- .../com/egm/stellio/shared/web/AuthUtils.kt | 19 ++++++ 7 files changed, 96 insertions(+), 74 deletions(-) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectAccessRights.kt b/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectAccessRights.kt index 6667f94c7..030ad278d 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectAccessRights.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectAccessRights.kt @@ -1,20 +1,15 @@ package com.egm.stellio.search.model -import java.net.URI +import com.egm.stellio.shared.web.SubjectType +import java.util.UUID data class SubjectAccessRights( - val subjectId: URI, + val subjectId: UUID, val subjectType: SubjectType, val globalRole: String? = null, val allowedReadEntities: Array? = null, val allowedWriteEntities: Array? = null ) { - enum class SubjectType { - USER, - GROUP, - CLIENT - } - override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -30,8 +25,3 @@ data class SubjectAccessRights( return subjectId.hashCode() } } - -fun getSubjectTypeFromSubjectId(subjectId: URI): SubjectAccessRights.SubjectType { - val type = subjectId.toString().split(":")[2] - return SubjectAccessRights.SubjectType.valueOf(type.toUpperCase()) -} diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt index 34ce7149e..ef36bb935 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt @@ -1,7 +1,6 @@ package com.egm.stellio.search.service import com.egm.stellio.search.model.SubjectAccessRights -import com.egm.stellio.search.model.getSubjectTypeFromSubjectId import com.egm.stellio.shared.model.AttributeAppendEvent import com.egm.stellio.shared.model.AttributeDeleteEvent import com.egm.stellio.shared.model.AttributeReplaceEvent @@ -10,6 +9,8 @@ import com.egm.stellio.shared.model.EntityDeleteEvent import com.egm.stellio.shared.model.EntityEvent import com.egm.stellio.shared.util.JsonUtils import com.egm.stellio.shared.util.toUri +import com.egm.stellio.shared.web.extractSubjectUuid +import com.egm.stellio.shared.web.getSubjectTypeFromSubjectId import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.slf4j.LoggerFactory @@ -46,7 +47,7 @@ class IAMListener( private fun createSubjectAccessRights(entityCreateEvent: EntityCreateEvent) { val userAccessRights = SubjectAccessRights( - subjectId = entityCreateEvent.entityId, + subjectId = entityCreateEvent.entityId.extractSubjectUuid(), subjectType = getSubjectTypeFromSubjectId(entityCreateEvent.entityId) ) @@ -57,7 +58,7 @@ class IAMListener( } private fun deleteSubjectAccessRights(entityDeleteEvent: EntityDeleteEvent) { - subjectAccessRightsService.delete(entityDeleteEvent.entityId) + subjectAccessRightsService.delete(entityDeleteEvent.entityId.extractSubjectUuid()) .subscribe { logger.debug("Deleted subject ${entityDeleteEvent.entityId}") } @@ -75,9 +76,9 @@ class IAMListener( } } if (hasStellioAdminRole) - subjectAccessRightsService.addAdminGlobalRole(attributeAppendEvent.entityId) + subjectAccessRightsService.addAdminGlobalRole(attributeAppendEvent.entityId.extractSubjectUuid()) else - subjectAccessRightsService.removeAdminGlobalRole(attributeAppendEvent.entityId) + subjectAccessRightsService.removeAdminGlobalRole(attributeAppendEvent.entityId.extractSubjectUuid()) } } @@ -86,10 +87,16 @@ class IAMListener( val entityId = operationPayloadNode["object"].asText() when (attributeAppendEvent.attributeName) { "rCanRead" -> { - subjectAccessRightsService.addReadRoleOnEntity(attributeAppendEvent.entityId, entityId.toUri()) + subjectAccessRightsService.addReadRoleOnEntity( + attributeAppendEvent.entityId.extractSubjectUuid(), + entityId.toUri() + ) } "rCanWrite" -> { - subjectAccessRightsService.addWriteRoleOnEntity(attributeAppendEvent.entityId, entityId.toUri()) + subjectAccessRightsService.addWriteRoleOnEntity( + attributeAppendEvent.entityId.extractSubjectUuid(), + entityId.toUri() + ) } else -> logger.warn("Unrecognized attribute name ${attributeAppendEvent.attributeName}") } @@ -97,7 +104,7 @@ class IAMListener( private fun removeEntityFromSubject(attributeDeleteEvent: AttributeDeleteEvent) { subjectAccessRightsService.removeRoleOnEntity( - attributeDeleteEvent.entityId, + attributeDeleteEvent.entityId.extractSubjectUuid(), attributeDeleteEvent.attributeName.toUri() ) } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt index eacdb064c..00811e3bf 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt @@ -1,13 +1,14 @@ package com.egm.stellio.search.service import com.egm.stellio.search.model.SubjectAccessRights -import com.egm.stellio.shared.util.toUri +import com.egm.stellio.shared.web.SubjectType import org.springframework.data.r2dbc.core.DatabaseClient import org.springframework.data.r2dbc.core.bind import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import reactor.core.publisher.Mono import java.net.URI +import java.util.UUID @Service class SubjectAccessRightsService( @@ -33,7 +34,7 @@ class SubjectAccessRightsService( .thenReturn(1) .onErrorReturn(-1) - fun retrieve(subjectId: URI): Mono = + fun retrieve(subjectId: UUID): Mono = databaseClient.execute( """ SELECT * @@ -47,7 +48,7 @@ class SubjectAccessRightsService( .map { rowToUserAccessRights(it) } @Transactional - fun addReadRoleOnEntity(subjectId: URI, entityId: URI): Mono = + fun addReadRoleOnEntity(subjectId: UUID, entityId: URI): Mono = databaseClient.execute( """ UPDATE subject_access_rights @@ -63,7 +64,7 @@ class SubjectAccessRightsService( .onErrorReturn(-1) @Transactional - fun addWriteRoleOnEntity(subjectId: URI, entityId: URI): Mono = + fun addWriteRoleOnEntity(subjectId: UUID, entityId: URI): Mono = databaseClient.execute( """ UPDATE subject_access_rights @@ -79,7 +80,7 @@ class SubjectAccessRightsService( .onErrorReturn(-1) @Transactional - fun removeRoleOnEntity(subjectId: URI, entityId: URI): Mono = + fun removeRoleOnEntity(subjectId: UUID, entityId: URI): Mono = databaseClient.execute( """ UPDATE subject_access_rights @@ -95,7 +96,8 @@ class SubjectAccessRightsService( .thenReturn(1) .onErrorReturn(-1) - fun addAdminGlobalRole(subjectId: URI): Mono = + @Transactional + fun addAdminGlobalRole(subjectId: UUID): Mono = databaseClient.execute( """ UPDATE subject_access_rights @@ -109,7 +111,8 @@ class SubjectAccessRightsService( .thenReturn(1) .onErrorReturn(-1) - fun removeAdminGlobalRole(subjectId: URI): Mono = + @Transactional + fun removeAdminGlobalRole(subjectId: UUID): Mono = databaseClient.execute( """ UPDATE subject_access_rights @@ -123,7 +126,7 @@ class SubjectAccessRightsService( .thenReturn(1) .onErrorReturn(-1) - fun hasReadRoleOnEntity(subjectId: URI, entityId: URI): Mono = + fun hasReadRoleOnEntity(subjectId: UUID, entityId: URI): Mono = databaseClient.execute( """ SELECT COUNT(subject_id) as count @@ -142,7 +145,7 @@ class SubjectAccessRightsService( .onErrorReturn(false) @Transactional - fun delete(subjectId: URI): Mono = + fun delete(subjectId: UUID): Mono = databaseClient.execute("DELETE FROM subject_access_rights WHERE subject_id = :subject_id") .bind("subject_id", subjectId) .fetch() @@ -151,8 +154,8 @@ class SubjectAccessRightsService( private fun rowToUserAccessRights(row: Map) = SubjectAccessRights( - subjectId = (row["subject_id"] as String).toUri(), - subjectType = SubjectAccessRights.SubjectType.valueOf(row["subject_type"] as String), + subjectId = row["subject_id"] as UUID, + subjectType = SubjectType.valueOf(row["subject_type"] as String), globalRole = row["global_role"] as String?, allowedReadEntities = (row["allowed_read_entities"] as Array?), allowedWriteEntities = (row["allowed_write_entities"] as Array?) diff --git a/search-service/src/main/resources/db/migration/V0_17__add_user_access_rights_table.sql b/search-service/src/main/resources/db/migration/V0_17__add_user_access_rights_table.sql index 659370eb7..b59749d32 100644 --- a/search-service/src/main/resources/db/migration/V0_17__add_user_access_rights_table.sql +++ b/search-service/src/main/resources/db/migration/V0_17__add_user_access_rights_table.sql @@ -1,5 +1,5 @@ CREATE TABLE subject_access_rights( - subject_id VARCHAR(64) NOT NULL PRIMARY KEY, + subject_id UUID NOT NULL PRIMARY KEY, subject_type VARCHAR(64) NOT NULL, global_role VARCHAR(64), allowed_read_entities TEXT[], diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt index b034f596a..ef764b706 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt @@ -1,8 +1,9 @@ package com.egm.stellio.search.service -import com.egm.stellio.search.model.SubjectAccessRights import com.egm.stellio.shared.util.loadSampleData import com.egm.stellio.shared.util.toUri +import com.egm.stellio.shared.web.SubjectType +import com.egm.stellio.shared.web.toUUID import com.ninjasquad.springmockk.MockkBean import io.mockk.confirmVerified import io.mockk.verify @@ -30,8 +31,8 @@ class IAMListenerTests { verify { subjectAccessRightsService.create( match { - it.subjectId == "urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0".toUri() && - it.subjectType == SubjectAccessRights.SubjectType.USER && + it.subjectId == "6ad19fe0-fc11-4024-85f2-931c6fa6f7e0".toUUID() && + it.subjectType == SubjectType.USER && it.globalRole == null && it.allowedReadEntities.isNullOrEmpty() && it.allowedWriteEntities.isNullOrEmpty() @@ -50,7 +51,7 @@ class IAMListenerTests { verify { subjectAccessRightsService.delete( match { - it == "urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0".toUri() + it == "6ad19fe0-fc11-4024-85f2-931c6fa6f7e0".toUUID() } ) } @@ -66,7 +67,7 @@ class IAMListenerTests { verify { subjectAccessRightsService.addAdminGlobalRole( match { - it == "urn:ngsi-ld:Group:ab67edf3-238c-4f50-83f4-617c620c62eb".toUri() + it == "ab67edf3-238c-4f50-83f4-617c620c62eb".toUUID() } ) } @@ -82,7 +83,7 @@ class IAMListenerTests { verify { subjectAccessRightsService.addAdminGlobalRole( match { - it == "urn:ngsi-ld:Client:ab67edf3-238c-4f50-83f4-617c620c62eb".toUri() + it == "ab67edf3-238c-4f50-83f4-617c620c62eb".toUUID() } ) } @@ -98,7 +99,7 @@ class IAMListenerTests { verify { subjectAccessRightsService.addAdminGlobalRole( match { - it == "urn:ngsi-ld:Group:ab67edf3-238c-4f50-83f4-617c620c62eb".toUri() + it == "ab67edf3-238c-4f50-83f4-617c620c62eb".toUUID() } ) } @@ -114,7 +115,7 @@ class IAMListenerTests { verify { subjectAccessRightsService.removeAdminGlobalRole( match { - it == "urn:ngsi-ld:Group:ab67edf3-238c-4f50-83f4-617c620c62eb".toUri() + it == "ab67edf3-238c-4f50-83f4-617c620c62eb".toUUID() } ) } @@ -129,7 +130,7 @@ class IAMListenerTests { verify { subjectAccessRightsService.addReadRoleOnEntity( - eq("urn:ngsi-ld:User:312b30b4-9279-4f7e-bdc5-ec56d699bb7d".toUri()), + eq("312b30b4-9279-4f7e-bdc5-ec56d699bb7d".toUUID()), eq("urn:ngsi-ld:Beekeeper:01".toUri()) ) } @@ -144,7 +145,7 @@ class IAMListenerTests { verify { subjectAccessRightsService.removeRoleOnEntity( - eq("urn:ngsi-ld:User:312b30b4-9279-4f7e-bdc5-ec56d699bb7d".toUri()), + eq("312b30b4-9279-4f7e-bdc5-ec56d699bb7d".toUUID()), eq("urn:ngsi-ld:Beekeeper:01".toUri()) ) } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt index 399c64332..d3cdbbcc4 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt @@ -3,6 +3,7 @@ package com.egm.stellio.search.service import com.egm.stellio.search.config.TimescaleBasedTests import com.egm.stellio.search.model.SubjectAccessRights import com.egm.stellio.shared.util.toUri +import com.egm.stellio.shared.web.SubjectType import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired @@ -10,6 +11,7 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.data.r2dbc.core.DatabaseClient import org.springframework.test.context.ActiveProfiles import reactor.test.StepVerifier +import java.util.UUID @SpringBootTest @ActiveProfiles("test") @@ -21,7 +23,7 @@ class SubjectAccessRightsServiceTests : TimescaleBasedTests() { @Autowired private lateinit var databaseClient: DatabaseClient - private val userUri = "urn:ngsi-ld:User:0123".toUri() + private val subjectUuid = UUID.fromString("0768A6D5-D87B-4209-9A22-8C40A8961A79") @AfterEach fun clearUsersAccessRightsTable() { @@ -35,8 +37,8 @@ class SubjectAccessRightsServiceTests : TimescaleBasedTests() { @Test fun `it should persist an user access right`() { val userAccessRights = SubjectAccessRights( - subjectId = userUri, - subjectType = SubjectAccessRights.SubjectType.USER, + subjectId = subjectUuid, + subjectType = SubjectType.USER, globalRole = "stellio-admin", allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678"), allowedWriteEntities = arrayOf("urn:ngsi-ld:Entity:6666", "urn:ngsi-ld:Entity:0000") @@ -55,8 +57,8 @@ class SubjectAccessRightsServiceTests : TimescaleBasedTests() { val allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678") val allowedWriteEntities = arrayOf("urn:ngsi-ld:Entity:6666", "urn:ngsi-ld:Entity:0000") val userAccessRights = SubjectAccessRights( - subjectId = userUri, - subjectType = SubjectAccessRights.SubjectType.USER, + subjectId = subjectUuid, + subjectType = SubjectType.USER, globalRole = "stellio-admin", allowedReadEntities = allowedReadEntities, allowedWriteEntities = allowedWriteEntities @@ -65,11 +67,11 @@ class SubjectAccessRightsServiceTests : TimescaleBasedTests() { subjectAccessRightsService.create(userAccessRights).block() StepVerifier.create( - subjectAccessRightsService.retrieve(userUri) + subjectAccessRightsService.retrieve(subjectUuid) ) .expectNextMatches { - it.subjectId == userUri && - it.subjectType == SubjectAccessRights.SubjectType.USER && + it.subjectId == subjectUuid && + it.subjectType == SubjectType.USER && it.globalRole == "stellio-admin" && it.allowedReadEntities?.contentEquals(allowedReadEntities) == true && it.allowedWriteEntities?.contentEquals(allowedWriteEntities) == true @@ -81,8 +83,8 @@ class SubjectAccessRightsServiceTests : TimescaleBasedTests() { @Test fun `it should add a new entity in the allowed list of read entities`() { val userAccessRights = SubjectAccessRights( - subjectId = userUri, - subjectType = SubjectAccessRights.SubjectType.USER, + subjectId = subjectUuid, + subjectType = SubjectType.USER, globalRole = "stellio-admin", allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678") ) @@ -90,14 +92,14 @@ class SubjectAccessRightsServiceTests : TimescaleBasedTests() { subjectAccessRightsService.create(userAccessRights).block() StepVerifier.create( - subjectAccessRightsService.addReadRoleOnEntity(userUri, "urn:ngsi-ld:Entity:1111".toUri()) + subjectAccessRightsService.addReadRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:1111".toUri()) ) .expectNextMatches { it == 1 } .expectComplete() .verify() StepVerifier.create( - subjectAccessRightsService.hasReadRoleOnEntity(userUri, "urn:ngsi-ld:Entity:1111".toUri()) + subjectAccessRightsService.hasReadRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:1111".toUri()) ) .expectNextMatches { it == true } .expectComplete() @@ -107,8 +109,8 @@ class SubjectAccessRightsServiceTests : TimescaleBasedTests() { @Test fun `it should remove an entity from the allowed list of read entities`() { val userAccessRights = SubjectAccessRights( - subjectId = userUri, - subjectType = SubjectAccessRights.SubjectType.USER, + subjectId = subjectUuid, + subjectType = SubjectType.USER, globalRole = "stellio-admin", allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678") ) @@ -116,14 +118,14 @@ class SubjectAccessRightsServiceTests : TimescaleBasedTests() { subjectAccessRightsService.create(userAccessRights).block() StepVerifier.create( - subjectAccessRightsService.removeRoleOnEntity(userUri, "urn:ngsi-ld:Entity:1234".toUri()) + subjectAccessRightsService.removeRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:1234".toUri()) ) .expectNextMatches { it == 1 } .expectComplete() .verify() StepVerifier.create( - subjectAccessRightsService.hasReadRoleOnEntity(userUri, "urn:ngsi-ld:Entity:1234".toUri()) + subjectAccessRightsService.hasReadRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:1234".toUri()) ) .expectNextMatches { it == false } .expectComplete() @@ -133,8 +135,8 @@ class SubjectAccessRightsServiceTests : TimescaleBasedTests() { @Test fun `it should update the global role of a subject`() { val userAccessRights = SubjectAccessRights( - subjectId = userUri, - subjectType = SubjectAccessRights.SubjectType.USER, + subjectId = subjectUuid, + subjectType = SubjectType.USER, allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678"), allowedWriteEntities = arrayOf("urn:ngsi-ld:Entity:6666") ) @@ -142,14 +144,14 @@ class SubjectAccessRightsServiceTests : TimescaleBasedTests() { subjectAccessRightsService.create(userAccessRights).block() StepVerifier.create( - subjectAccessRightsService.addAdminGlobalRole(userUri) + subjectAccessRightsService.addAdminGlobalRole(subjectUuid) ) .expectNextMatches { it == 1 } .expectComplete() .verify() StepVerifier.create( - subjectAccessRightsService.retrieve(userUri) + subjectAccessRightsService.retrieve(subjectUuid) ) .expectNextMatches { it.globalRole == "stellio-admin" @@ -158,14 +160,14 @@ class SubjectAccessRightsServiceTests : TimescaleBasedTests() { .verify() StepVerifier.create( - subjectAccessRightsService.removeAdminGlobalRole(userUri) + subjectAccessRightsService.removeAdminGlobalRole(subjectUuid) ) .expectNextMatches { it == 1 } .expectComplete() .verify() StepVerifier.create( - subjectAccessRightsService.retrieve(userUri) + subjectAccessRightsService.retrieve(subjectUuid) ) .expectNextMatches { it.globalRole == null @@ -177,8 +179,8 @@ class SubjectAccessRightsServiceTests : TimescaleBasedTests() { @Test fun `it should find if an user has a read role on a entity`() { val userAccessRights = SubjectAccessRights( - subjectId = userUri, - subjectType = SubjectAccessRights.SubjectType.USER, + subjectId = subjectUuid, + subjectType = SubjectType.USER, globalRole = "stellio-admin", allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678"), allowedWriteEntities = arrayOf("urn:ngsi-ld:Entity:6666") @@ -187,21 +189,21 @@ class SubjectAccessRightsServiceTests : TimescaleBasedTests() { subjectAccessRightsService.create(userAccessRights).block() StepVerifier.create( - subjectAccessRightsService.hasReadRoleOnEntity(userUri, "urn:ngsi-ld:Entity:1234".toUri()) + subjectAccessRightsService.hasReadRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:1234".toUri()) ) .expectNextMatches { it == true } .expectComplete() .verify() StepVerifier.create( - subjectAccessRightsService.hasReadRoleOnEntity(userUri, "urn:ngsi-ld:Entity:1111".toUri()) + subjectAccessRightsService.hasReadRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:1111".toUri()) ) .expectNextMatches { it == false } .expectComplete() .verify() StepVerifier.create( - subjectAccessRightsService.hasReadRoleOnEntity(userUri, "urn:ngsi-ld:Entity:6666".toUri()) + subjectAccessRightsService.hasReadRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:6666".toUri()) ) .expectNextMatches { it == true } .expectComplete() @@ -211,8 +213,8 @@ class SubjectAccessRightsServiceTests : TimescaleBasedTests() { @Test fun `it should delete an user access right`() { val userAccessRights = SubjectAccessRights( - subjectId = userUri, - subjectType = SubjectAccessRights.SubjectType.USER, + subjectId = subjectUuid, + subjectType = SubjectType.USER, globalRole = "stellio-admin", allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678"), allowedWriteEntities = arrayOf("urn:ngsi-ld:Entity:6666", "urn:ngsi-ld:Entity:0000") @@ -221,14 +223,14 @@ class SubjectAccessRightsServiceTests : TimescaleBasedTests() { subjectAccessRightsService.create(userAccessRights).block() StepVerifier.create( - subjectAccessRightsService.delete(userUri) + subjectAccessRightsService.delete(subjectUuid) ) .expectNextMatches { it == 1 } .expectComplete() .verify() StepVerifier.create( - subjectAccessRightsService.retrieve(userUri) + subjectAccessRightsService.retrieve(subjectUuid) ) .expectComplete() .verify() diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/web/AuthUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/web/AuthUtils.kt index 27febec87..b87604746 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/web/AuthUtils.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/web/AuthUtils.kt @@ -4,9 +4,28 @@ import org.springframework.security.core.context.ReactiveSecurityContextHolder import org.springframework.security.core.context.SecurityContextImpl import org.springframework.security.oauth2.jwt.Jwt import reactor.core.publisher.Mono +import java.net.URI +import java.util.UUID fun extractSubjectOrEmpty(): Mono { return ReactiveSecurityContextHolder.getContext() .switchIfEmpty(Mono.just(SecurityContextImpl())) .map { context -> context.authentication?.principal?.let { (it as Jwt).subject } ?: "" } } + +fun URI.extractSubjectUuid(): UUID = + UUID.fromString(this.toString().substringAfterLast(":")) + +fun String.toUUID(): UUID = + UUID.fromString(this) + +fun getSubjectTypeFromSubjectId(subjectId: URI): SubjectType { + val type = subjectId.toString().split(":")[2] + return SubjectType.valueOf(type.toUpperCase()) +} + +enum class SubjectType { + USER, + GROUP, + CLIENT +} From 0b660f221a6055424075e72c3db7c8e82087b4b9 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Wed, 23 Jun 2021 22:35:10 +0200 Subject: [PATCH 08/28] feat(search): add access control when getting temporal evolution of an entity --- search-service/config/detekt/baseline.xml | 1 - .../search/web/TemporalEntityHandler.kt | 14 ++++- .../search/web/TemporalEntityHandlerTests.kt | 63 +++++++++++++++---- 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/search-service/config/detekt/baseline.xml b/search-service/config/detekt/baseline.xml index a6bf5e45a..5b1e57dbe 100644 --- a/search-service/config/detekt/baseline.xml +++ b/search-service/config/detekt/baseline.xml @@ -3,7 +3,6 @@ LargeClass:EntityEventListenerServiceTest.kt$EntityEventListenerServiceTest - LargeClass:TemporalEntityHandlerTests.kt$TemporalEntityHandlerTests LongMethod:ParameterizedTests.kt$ParameterizedTests.Companion$@JvmStatic fun rawResultsProvider(): Stream<Arguments> LongMethod:QueryServiceTests.kt$QueryServiceTests$@Test fun `it should query temporal entities as requested by query params`() LongParameterList:AttributeInstance.kt$AttributeInstance.Companion$( temporalEntityAttribute: UUID, instanceId: URI? = null, observedAt: ZonedDateTime, value: String? = null, measuredValue: Double? = null, jsonNode: JsonNode ) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt index a3f0a5e01..6a92a99a7 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt @@ -8,7 +8,9 @@ import arrow.core.right import com.egm.stellio.search.model.TemporalQuery import com.egm.stellio.search.service.AttributeInstanceService import com.egm.stellio.search.service.QueryService +import com.egm.stellio.search.service.SubjectAccessRightsService import com.egm.stellio.search.service.TemporalEntityAttributeService +import com.egm.stellio.shared.model.AccessDeniedException import com.egm.stellio.shared.model.BadRequestDataException import com.egm.stellio.shared.model.BadRequestDataResponse import com.egm.stellio.shared.model.getDatasetId @@ -18,6 +20,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.compactTerm import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdFragment import com.egm.stellio.shared.util.JsonLdUtils.expandValueAsListOfMap import com.egm.stellio.shared.util.JsonUtils.serializeObject +import com.egm.stellio.shared.web.extractSubjectOrEmpty import kotlinx.coroutines.reactive.awaitFirst import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus @@ -35,13 +38,15 @@ import org.springframework.web.bind.annotation.RestController import reactor.core.publisher.Mono import java.time.ZonedDateTime import java.util.Optional +import java.util.UUID @RestController @RequestMapping("/ngsi-ld/v1/temporal/entities") class TemporalEntityHandler( private val attributeInstanceService: AttributeInstanceService, private val temporalEntityAttributeService: TemporalEntityAttributeService, - private val queryService: QueryService + private val queryService: QueryService, + private val subjectAccessRightsService: SubjectAccessRightsService ) { /** @@ -131,6 +136,13 @@ class TemporalEntityHandler( @PathVariable entityId: String, @RequestParam params: MultiValueMap ): ResponseEntity<*> { + val userId = extractSubjectOrEmpty().awaitFirst() + + val canReadEntity = + subjectAccessRightsService.hasReadRoleOnEntity(UUID.fromString(userId), entityId.toUri()).awaitFirst() + if (!canReadEntity) + throw AccessDeniedException("User forbidden read access to entity $entityId") + val withTemporalValues = hasValueInOptionsParam(Optional.ofNullable(params.getFirst("options")), OptionsParamValue.TEMPORAL_VALUES) val contextLink = getContextFromLinkHeaderOrDefault(httpHeaders) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt index a40ca8dd0..5faa00027 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt @@ -7,11 +7,19 @@ import com.egm.stellio.search.model.TemporalEntityAttribute import com.egm.stellio.search.model.TemporalQuery import com.egm.stellio.search.service.AttributeInstanceService import com.egm.stellio.search.service.QueryService +import com.egm.stellio.search.service.SubjectAccessRightsService import com.egm.stellio.search.service.TemporalEntityAttributeService +import com.egm.stellio.shared.WithMockCustomUser import com.egm.stellio.shared.model.BadRequestDataException import com.egm.stellio.shared.model.ResourceNotFoundException -import com.egm.stellio.shared.util.* +import com.egm.stellio.shared.util.APIC_COMPOUND_CONTEXT +import com.egm.stellio.shared.util.JSON_LD_MEDIA_TYPE import com.egm.stellio.shared.util.JsonUtils.deserializeObject +import com.egm.stellio.shared.util.RESULTS_COUNT_HEADER +import com.egm.stellio.shared.util.buildContextLinkHeader +import com.egm.stellio.shared.util.entityNotFoundMessage +import com.egm.stellio.shared.util.loadSampleData +import com.egm.stellio.shared.util.toUri import com.ninjasquad.springmockk.MockkBean import io.mockk.coEvery import io.mockk.coVerify @@ -28,7 +36,6 @@ import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest import org.springframework.context.annotation.Import import org.springframework.http.MediaType import org.springframework.security.test.context.support.WithAnonymousUser -import org.springframework.security.test.context.support.WithMockUser import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.util.LinkedMultiValueMap @@ -41,7 +48,7 @@ import java.util.UUID @ActiveProfiles("test") @WebFluxTest(TemporalEntityHandler::class) @Import(WebSecurityTestConfig::class) -@WithMockUser +@WithMockCustomUser(name = "Mock User", username = "0768A6D5-D87B-4209-9A22-8C40A8961A79") class TemporalEntityHandlerTests { private val incomingAttrExpandedName = "https://ontology.eglobalmark.com/apic#incoming" @@ -61,6 +68,9 @@ class TemporalEntityHandlerTests { @MockkBean(relaxed = true) private lateinit var temporalEntityAttributeService: TemporalEntityAttributeService + @MockkBean + private lateinit var subjectAccessRightsService: SubjectAccessRightsService + private val entityUri = "urn:ngsi-ld:BeeHive:TESTC".toUri() @BeforeAll @@ -267,8 +277,10 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if timerel is present without time query param`() { + every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + webClient.get() - .uri("/ngsi-ld/v1/temporal/entities/entityId?timerel=before") + .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=before") .exchange() .expectStatus().isBadRequest .expectBody().json( @@ -284,8 +296,10 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if time is present without timerel query param`() { + every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + webClient.get() - .uri("/ngsi-ld/v1/temporal/entities/entityId?time=2020-10-29T18:00:00Z") + .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?time=2020-10-29T18:00:00Z") .exchange() .expectStatus().isBadRequest .expectBody().json( @@ -302,6 +316,8 @@ class TemporalEntityHandlerTests { @Test fun `it should give a 200 if no timerel and no time query params are in the request`() { coEvery { queryService.queryTemporalEntity(any(), any(), any(), any()) } returns emptyMap() + every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + webClient.get() .uri("/ngsi-ld/v1/temporal/entities/$entityUri") .exchange() @@ -310,8 +326,10 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if timerel is between and no endTime provided`() { + every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + webClient.get() - .uri("/ngsi-ld/v1/temporal/entities/entityId?timerel=between&time=startTime") + .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=between&time=startTime") .exchange() .expectStatus().isBadRequest .expectBody().json( @@ -326,9 +344,11 @@ class TemporalEntityHandlerTests { } @Test - fun `it should raise a 400 if time is not parseable`() { + fun `it should raise a 400 if time is not parsable`() { + every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + webClient.get() - .uri("/ngsi-ld/v1/temporal/entities/entityId?timerel=before&time=badTime") + .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=before&time=badTime") .exchange() .expectStatus().isBadRequest .expectBody().json( @@ -344,8 +364,10 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if timerel is not a valid value`() { + every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + webClient.get() - .uri("/ngsi-ld/v1/temporal/entities/entityId?timerel=befor&time=badTime") + .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=befor&time=badTime") .exchange() .expectStatus().isBadRequest .expectBody().json( @@ -361,8 +383,13 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if timerel is between and endTime is not parseable`() { + every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + webClient.get() - .uri("/ngsi-ld/v1/temporal/entities/entityId?timerel=between&time=2019-10-17T07:31:39Z&endTime=endTime") + .uri( + "/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?" + + "timerel=between&time=2019-10-17T07:31:39Z&endTime=endTime" + ) .exchange() .expectStatus().isBadRequest .expectBody().json( @@ -378,8 +405,13 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if one of time bucket or aggregate is missing`() { + every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + webClient.get() - .uri("/ngsi-ld/v1/temporal/entities/entityId?timerel=after&time=2020-01-31T07:31:39Z&timeBucket=1 minute") + .uri( + "/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?" + + "timerel=after&time=2020-01-31T07:31:39Z&timeBucket=1 minute" + ) .exchange() .expectStatus().isBadRequest .expectBody().json( @@ -395,9 +427,11 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if aggregate function is unknown`() { + every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + webClient.get() .uri( - "/ngsi-ld/v1/temporal/entities/entityId?" + + "/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?" + "timerel=after&time=2020-01-31T07:31:39Z&timeBucket=1 minute&aggregate=unknown" ) .exchange() @@ -415,6 +449,7 @@ class TemporalEntityHandlerTests { @Test fun `it should return a 404 if temporal entity attribute does not exist`() { + every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } coEvery { queryService.queryTemporalEntity(any(), any(), any(), any()) } throws ResourceNotFoundException("Entity urn:ngsi-ld:BeeHive:TESTC was not found") @@ -440,6 +475,7 @@ class TemporalEntityHandlerTests { @Test fun `it should return a 200 if minimal required parameters are valid`() { coEvery { queryService.queryTemporalEntity(any(), any(), any(), any()) } returns emptyMap() + every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri( @@ -468,6 +504,7 @@ class TemporalEntityHandlerTests { @Test fun `it should return an entity with two temporal properties evolution`() { mockWithIncomingAndOutgoingTemporalProperties(false) + every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri( @@ -488,6 +525,7 @@ class TemporalEntityHandlerTests { @Test fun `it should return a json entity with two temporal properties evolution`() { mockWithIncomingAndOutgoingTemporalProperties(false) + every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri( @@ -510,6 +548,7 @@ class TemporalEntityHandlerTests { @Test fun `it should return an entity with two temporal properties evolution with temporalValues option`() { mockWithIncomingAndOutgoingTemporalProperties(true) + every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri( From 63b69bebae28afbacf57ee9f1e3340271ba56692 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Thu, 24 Jun 2021 18:22:29 +0200 Subject: [PATCH 09/28] feat(api-gateway): add route for access control API --- .../com/egm/stellio/apigateway/ApiGatewayApplication.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/api-gateway/src/main/kotlin/com/egm/stellio/apigateway/ApiGatewayApplication.kt b/api-gateway/src/main/kotlin/com/egm/stellio/apigateway/ApiGatewayApplication.kt index 3251068c0..05259218c 100644 --- a/api-gateway/src/main/kotlin/com/egm/stellio/apigateway/ApiGatewayApplication.kt +++ b/api-gateway/src/main/kotlin/com/egm/stellio/apigateway/ApiGatewayApplication.kt @@ -35,6 +35,13 @@ class ApiGatewayApplication { } .uri("http://$entityServiceUrl:8082") } + .route { p -> + p.path("/ngsi-ld/v1/entityAccessControl/**") + .filters { + it.filter(filterFactory.apply()) + } + .uri("http://$entityServiceUrl:8082") + } .route { p -> p.path("/ngsi-ld/v1/types/**") .filters { From 9699e8d0f93bee31965003365f815713b1ce12ca Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Sun, 28 Nov 2021 11:25:08 +0100 Subject: [PATCH 10/28] migrate code to last version (new SB version, refactored event model, ...) --- .../apigateway/ApiGatewayApplication.kt | 2 +- .../Neo4jAuthorizationRepository.kt | 6 +-- .../entity/web/EntityAccessControlHandler.kt | 4 ++ .../web/EntityAccessControlHandlerTests.kt | 16 +++++++ search-service/config/detekt/baseline.xml | 1 + .../search/model/SubjectAccessRights.kt | 2 + .../egm/stellio/search/service/IAMListener.kt | 2 +- .../service/SubjectAccessRightsService.kt | 44 +++++++++++-------- .../SubjectAccessRightsServiceTests.kt | 14 +++--- .../GroupMembershipAppendEvent.json | 3 +- .../GroupMembershipDeleteEvent.json | 1 + .../authorization/GroupUpdateEvent.json | 3 +- .../RealmRoleAppendEventNoRole.json | 3 +- .../RealmRoleAppendEventOneRole.json | 3 +- .../RealmRoleAppendEventTwoRoles.json | 3 +- .../RealmRoleAppendToClient.json | 3 +- .../authorization/RightAddOnEntity.json | 1 + .../authorization/RightRemoveOnEntity.json | 1 + .../ngsild/authorization/UserCreateEvent.json | 1 + .../ngsild/authorization/UserDeleteEvent.json | 1 + .../com/egm/stellio/shared/web/AuthUtils.kt | 3 +- 21 files changed, 79 insertions(+), 38 deletions(-) diff --git a/api-gateway/src/main/kotlin/com/egm/stellio/apigateway/ApiGatewayApplication.kt b/api-gateway/src/main/kotlin/com/egm/stellio/apigateway/ApiGatewayApplication.kt index 05259218c..47e35d6bb 100644 --- a/api-gateway/src/main/kotlin/com/egm/stellio/apigateway/ApiGatewayApplication.kt +++ b/api-gateway/src/main/kotlin/com/egm/stellio/apigateway/ApiGatewayApplication.kt @@ -38,7 +38,7 @@ class ApiGatewayApplication { .route { p -> p.path("/ngsi-ld/v1/entityAccessControl/**") .filters { - it.filter(filterFactory.apply()) + it.tokenRelay() } .uri("http://$entityServiceUrl:8082") } diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt index c2586c2bb..289d47a06 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt @@ -159,10 +159,10 @@ class Neo4jAuthorizationRepository( """.trimIndent() val parameters = mapOf( - "entityId" to subjectId, - "targetId" to targetId + "entityId" to subjectId.toString(), + "targetId" to targetId.toString() ) - return session.query(matchQuery, parameters).queryStatistics().nodesDeleted + return neo4jClient.query(matchQuery).bindAll(parameters).run().counters().nodesDeleted() } } diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt index 7fd9774b5..c1f6dd82a 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt @@ -77,6 +77,8 @@ class EntityAccessControlHandler( ) val attributeAppendEvent = AttributeAppendEvent( entityId = subjectId.toUri(), + // TODO costly and not exactly what we want + entityType = entityService.getEntityCoreProperties(subjectId.toUri()).type[0], attributeName = ngsiLdRelationship.compactName, datasetId = it.second.datasetId, operationPayload = JsonUtils.serializeObject(operationPayload), @@ -120,6 +122,8 @@ class EntityAccessControlHandler( if (removeResult == 1) { val attributeAppendEvent = AttributeDeleteEvent( entityId = subjectId.toUri(), + // TODO costly and not exactly what we want + entityType = entityService.getEntityCoreProperties(subjectId.toUri()).type[0], attributeName = entityId, datasetId = null, updatedEntity = "", diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt index a09ce9dcf..8e23016d6 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt @@ -4,6 +4,7 @@ import com.egm.stellio.entity.authorization.AuthorizationService import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_READ import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_WRITE import com.egm.stellio.entity.config.WebSecurityTestConfig +import com.egm.stellio.entity.model.Entity import com.egm.stellio.entity.service.EntityService import com.egm.stellio.shared.WithMockCustomUser import com.egm.stellio.shared.model.AttributeAppendEvent @@ -18,6 +19,7 @@ import com.egm.stellio.shared.util.matchContent import com.egm.stellio.shared.util.toUri import com.ninjasquad.springmockk.MockkBean import io.mockk.every +import io.mockk.mockk import io.mockk.verify import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test @@ -51,6 +53,7 @@ class EntityAccessControlHandlerTests { private val subjectId = "urn:ngsi-ld:User:0123".toUri() private val entityUri1 = "urn:ngsi-ld:Entity:entityId1".toUri() + private val entityType = "https://my.context/Entity" @BeforeAll fun configureWebClientDefaults() { @@ -64,6 +67,7 @@ class EntityAccessControlHandlerTests { @Test fun `it should allow an authorized user to give access to an entity`() { + val entity = mockk() val requestPayload = """ { @@ -76,6 +80,8 @@ class EntityAccessControlHandlerTests { """.trimIndent() every { authorizationService.userIsAdminOfEntity(any(), any()) } returns true + every { entityService.getEntityCoreProperties(any()) } returns entity + every { entity.type } returns listOf(entityType) every { kafkaTemplate.send(any(), any(), any()) } returns SettableListenableFuture() webClient.post() @@ -124,6 +130,7 @@ class EntityAccessControlHandlerTests { @Test fun `it should allow an authorized user to give access to a set of entities`() { + val entity = mockk() val entityUri2 = "urn:ngsi-ld:Entity:entityId2".toUri() val entityUri3 = "urn:ngsi-ld:Entity:entityId3".toUri() val requestPayload = @@ -148,6 +155,8 @@ class EntityAccessControlHandlerTests { """.trimIndent() every { authorizationService.userIsAdminOfEntity(any(), any()) } returns true + every { entityService.getEntityCoreProperties(any()) } returns entity + every { entity.type } returns listOf(entityType) webClient.post() .uri("/ngsi-ld/v1/entityAccessControl/$subjectId/attrs") @@ -186,6 +195,7 @@ class EntityAccessControlHandlerTests { @Test fun `it should only allow to give access to authorized entities`() { + val entity = mockk() val entityUri2 = "urn:ngsi-ld:Entity:entityId2".toUri() val requestPayload = """ @@ -206,6 +216,8 @@ class EntityAccessControlHandlerTests { every { authorizationService.userIsAdminOfEntity(eq(entityUri1), any()) } returns true every { authorizationService.userIsAdminOfEntity(eq(entityUri2), any()) } returns false + every { entityService.getEntityCoreProperties(any()) } returns entity + every { entity.type } returns listOf(entityType) webClient.post() .uri("/ngsi-ld/v1/entityAccessControl/$subjectId/attrs") @@ -230,8 +242,12 @@ class EntityAccessControlHandlerTests { @Test fun `it should allow an authorized user to remove access to an entity`() { + val entity = mockk() + every { authorizationService.userIsAdminOfEntity(any(), any()) } returns true every { authorizationService.removeUserRightsOnEntity(any(), any()) } returns 1 + every { entityService.getEntityCoreProperties(any()) } returns entity + every { entity.type } returns listOf(entityType) every { kafkaTemplate.send(any(), any(), any()) } returns SettableListenableFuture() webClient.delete() diff --git a/search-service/config/detekt/baseline.xml b/search-service/config/detekt/baseline.xml index 5b1e57dbe..a6bf5e45a 100644 --- a/search-service/config/detekt/baseline.xml +++ b/search-service/config/detekt/baseline.xml @@ -3,6 +3,7 @@ LargeClass:EntityEventListenerServiceTest.kt$EntityEventListenerServiceTest + LargeClass:TemporalEntityHandlerTests.kt$TemporalEntityHandlerTests LongMethod:ParameterizedTests.kt$ParameterizedTests.Companion$@JvmStatic fun rawResultsProvider(): Stream<Arguments> LongMethod:QueryServiceTests.kt$QueryServiceTests$@Test fun `it should query temporal entities as requested by query params`() LongParameterList:AttributeInstance.kt$AttributeInstance.Companion$( temporalEntityAttribute: UUID, instanceId: URI? = null, observedAt: ZonedDateTime, value: String? = null, measuredValue: Double? = null, jsonNode: JsonNode ) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectAccessRights.kt b/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectAccessRights.kt index 030ad278d..43b5d9876 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectAccessRights.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectAccessRights.kt @@ -1,9 +1,11 @@ package com.egm.stellio.search.model import com.egm.stellio.shared.web.SubjectType +import org.springframework.data.annotation.Id import java.util.UUID data class SubjectAccessRights( + @Id val subjectId: UUID, val subjectType: SubjectType, val globalRole: String? = null, diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt index ef36bb935..9a075b1c1 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt @@ -67,7 +67,7 @@ class IAMListener( private fun addRoleToSubject(attributeAppendEvent: AttributeAppendEvent) { if (attributeAppendEvent.attributeName == "roles") { val operationPayloadNode = jacksonObjectMapper().readTree(attributeAppendEvent.operationPayload) - val updatedRoles = (operationPayloadNode["roles"]["value"] as ArrayNode).elements() + val updatedRoles = (operationPayloadNode["value"] as ArrayNode).elements() var hasStellioAdminRole = false while (updatedRoles.hasNext()) { if (updatedRoles.next().asText().equals("stellio-admin")) { diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt index 00811e3bf..0afd87467 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt @@ -2,8 +2,12 @@ package com.egm.stellio.search.service import com.egm.stellio.search.model.SubjectAccessRights import com.egm.stellio.shared.web.SubjectType -import org.springframework.data.r2dbc.core.DatabaseClient -import org.springframework.data.r2dbc.core.bind +import org.slf4j.LoggerFactory +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate +import org.springframework.data.relational.core.query.Criteria +import org.springframework.data.relational.core.query.Query +import org.springframework.r2dbc.core.DatabaseClient +import org.springframework.r2dbc.core.bind import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import reactor.core.publisher.Mono @@ -12,12 +16,15 @@ import java.util.UUID @Service class SubjectAccessRightsService( - private val databaseClient: DatabaseClient + private val databaseClient: DatabaseClient, + private val r2dbcEntityTemplate: R2dbcEntityTemplate, ) { + private val logger = LoggerFactory.getLogger(javaClass) + @Transactional fun create(subjectAccessRights: SubjectAccessRights): Mono = - databaseClient.execute( + databaseClient.sql( """ INSERT INTO subject_access_rights (subject_id, subject_type, global_role, allowed_read_entities, allowed_write_entities) @@ -25,17 +32,20 @@ class SubjectAccessRightsService( """ ) .bind("subject_id", subjectAccessRights.subjectId) - .bind("subject_type", subjectAccessRights.subjectType) + .bind("subject_type", subjectAccessRights.subjectType.toString()) .bind("global_role", subjectAccessRights.globalRole) .bind("allowed_read_entities", subjectAccessRights.allowedReadEntities) .bind("allowed_write_entities", subjectAccessRights.allowedWriteEntities) .fetch() .rowsUpdated() .thenReturn(1) - .onErrorReturn(-1) + .onErrorResume { + logger.error("Error while creating a new subject access right : ${it.message}", it) + Mono.just(-1) + } fun retrieve(subjectId: UUID): Mono = - databaseClient.execute( + databaseClient.sql( """ SELECT * FROM subject_access_rights @@ -49,7 +59,7 @@ class SubjectAccessRightsService( @Transactional fun addReadRoleOnEntity(subjectId: UUID, entityId: URI): Mono = - databaseClient.execute( + databaseClient.sql( """ UPDATE subject_access_rights SET allowed_read_entities = array_append(allowed_read_entities, :entity_id::text) @@ -65,7 +75,7 @@ class SubjectAccessRightsService( @Transactional fun addWriteRoleOnEntity(subjectId: UUID, entityId: URI): Mono = - databaseClient.execute( + databaseClient.sql( """ UPDATE subject_access_rights SET allowed_write_entities = array_append(allowed_write_entities, :entity_id::text) @@ -81,7 +91,7 @@ class SubjectAccessRightsService( @Transactional fun removeRoleOnEntity(subjectId: UUID, entityId: URI): Mono = - databaseClient.execute( + databaseClient.sql( """ UPDATE subject_access_rights SET allowed_read_entities = array_remove(allowed_read_entities, :entity_id::text), @@ -98,7 +108,7 @@ class SubjectAccessRightsService( @Transactional fun addAdminGlobalRole(subjectId: UUID): Mono = - databaseClient.execute( + databaseClient.sql( """ UPDATE subject_access_rights SET global_role = 'stellio-admin' @@ -113,7 +123,7 @@ class SubjectAccessRightsService( @Transactional fun removeAdminGlobalRole(subjectId: UUID): Mono = - databaseClient.execute( + databaseClient.sql( """ UPDATE subject_access_rights SET global_role = null @@ -127,7 +137,7 @@ class SubjectAccessRightsService( .onErrorReturn(-1) fun hasReadRoleOnEntity(subjectId: UUID, entityId: URI): Mono = - databaseClient.execute( + databaseClient.sql( """ SELECT COUNT(subject_id) as count FROM subject_access_rights @@ -146,11 +156,9 @@ class SubjectAccessRightsService( @Transactional fun delete(subjectId: UUID): Mono = - databaseClient.execute("DELETE FROM subject_access_rights WHERE subject_id = :subject_id") - .bind("subject_id", subjectId) - .fetch() - .rowsUpdated() - .onErrorReturn(-1) + r2dbcEntityTemplate.delete(SubjectAccessRights::class.java) + .matching(Query.query(Criteria.where("subject_id").`is`(subjectId))) + .all() private fun rowToUserAccessRights(row: Map) = SubjectAccessRights( diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt index d3cdbbcc4..4e3588839 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt @@ -1,36 +1,34 @@ package com.egm.stellio.search.service -import com.egm.stellio.search.config.TimescaleBasedTests import com.egm.stellio.search.model.SubjectAccessRights +import com.egm.stellio.search.support.WithTimescaleContainer import com.egm.stellio.shared.util.toUri import com.egm.stellio.shared.web.SubjectType import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest -import org.springframework.data.r2dbc.core.DatabaseClient +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate import org.springframework.test.context.ActiveProfiles import reactor.test.StepVerifier import java.util.UUID @SpringBootTest @ActiveProfiles("test") -class SubjectAccessRightsServiceTests : TimescaleBasedTests() { +class SubjectAccessRightsServiceTests : WithTimescaleContainer { @Autowired private lateinit var subjectAccessRightsService: SubjectAccessRightsService @Autowired - private lateinit var databaseClient: DatabaseClient + private lateinit var r2dbcEntityTemplate: R2dbcEntityTemplate private val subjectUuid = UUID.fromString("0768A6D5-D87B-4209-9A22-8C40A8961A79") @AfterEach fun clearUsersAccessRightsTable() { - databaseClient.delete() - .from("subject_access_rights") - .fetch() - .rowsUpdated() + r2dbcEntityTemplate.delete(SubjectAccessRights::class.java) + .all() .block() } diff --git a/search-service/src/test/resources/ngsild/authorization/GroupMembershipAppendEvent.json b/search-service/src/test/resources/ngsild/authorization/GroupMembershipAppendEvent.json index e934fad19..f5b3f5756 100644 --- a/search-service/src/test/resources/ngsild/authorization/GroupMembershipAppendEvent.json +++ b/search-service/src/test/resources/ngsild/authorization/GroupMembershipAppendEvent.json @@ -1,9 +1,10 @@ { "operationType": "ATTRIBUTE_APPEND", "entityId": "urn:ngsi-ld:User:96e1f1e9-d798-48d7-820e-59f5a9a2abf5", + "entityType": "User", "attributeName": "isMemberOf", "datasetId": "urn:ngsi-ld:Dataset:7cdad168-96ee-4649-b768-a060ac2ef435", - "operationPayload": "{\"isMemberOf\":[{\"type\":\"Relationship\",\"datasetId\":\"urn:ngsi-ld:Dataset:7cdad168-96ee-4649-b768-a060ac2ef435\",\"object\":\"urn:ngsi-ld:Group:7cdad168-96ee-4649-b768-a060ac2ef435\"}]}", + "operationPayload": "{\"type\":\"Relationship\",\"datasetId\":\"urn:ngsi-ld:Dataset:7cdad168-96ee-4649-b768-a060ac2ef435\",\"object\":\"urn:ngsi-ld:Group:7cdad168-96ee-4649-b768-a060ac2ef435\"}", "updatedEntity": "", "contexts": [ "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld", diff --git a/search-service/src/test/resources/ngsild/authorization/GroupMembershipDeleteEvent.json b/search-service/src/test/resources/ngsild/authorization/GroupMembershipDeleteEvent.json index ad8cf1cd9..6ab86551f 100644 --- a/search-service/src/test/resources/ngsild/authorization/GroupMembershipDeleteEvent.json +++ b/search-service/src/test/resources/ngsild/authorization/GroupMembershipDeleteEvent.json @@ -1,6 +1,7 @@ { "operationType": "ATTRIBUTE_DELETE", "entityId": "urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0", + "entityType": "User", "attributeName": "isMemberOf", "datasetId": "urn:ngsi-ld:Dataset:isMemberOf:7cdad168-96ee-4649-b768-a060ac2ef435", "updatedEntity": "", diff --git a/search-service/src/test/resources/ngsild/authorization/GroupUpdateEvent.json b/search-service/src/test/resources/ngsild/authorization/GroupUpdateEvent.json index d340d3d27..7f2148214 100644 --- a/search-service/src/test/resources/ngsild/authorization/GroupUpdateEvent.json +++ b/search-service/src/test/resources/ngsild/authorization/GroupUpdateEvent.json @@ -1,8 +1,9 @@ { "operationType": "ATTRIBUTE_REPLACE", "entityId": "urn:ngsi-ld:Group:ab67edf3-238c-4f50-83f4-617c620c62eb", + "entityType": "Group", "attributeName": "name", - "operationPayload": "{\"name\":{\"type\":\"Property\",\"value\":\"EGM Team\"}}", + "operationPayload": "{\"type\":\"Property\",\"value\":\"EGM Team\"}", "updatedEntity": "", "contexts": [ "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld", diff --git a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventNoRole.json b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventNoRole.json index ab4695b75..9f7db5502 100644 --- a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventNoRole.json +++ b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventNoRole.json @@ -1,8 +1,9 @@ { "operationType":"ATTRIBUTE_APPEND", "entityId":"urn:ngsi-ld:Group:ab67edf3-238c-4f50-83f4-617c620c62eb", + "entityType": "Group", "attributeName": "roles", - "operationPayload":"{\"roles\":{\"type\":\"Property\",\"value\":[]}}", + "operationPayload":"{\"type\":\"Property\",\"value\":[]}", "updatedEntity": "", "contexts":["https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld","http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld"] } diff --git a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventOneRole.json b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventOneRole.json index df12ab5f5..1ef294c34 100644 --- a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventOneRole.json +++ b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventOneRole.json @@ -1,8 +1,9 @@ { "operationType":"ATTRIBUTE_APPEND", "entityId":"urn:ngsi-ld:Group:ab67edf3-238c-4f50-83f4-617c620c62eb", + "entityType": "Group", "attributeName": "roles", - "operationPayload":"{\"roles\":{\"type\":\"Property\",\"value\":[\"stellio-admin\"]}}", + "operationPayload":"{\"type\":\"Property\",\"value\":[\"stellio-admin\"]}", "updatedEntity": "", "contexts":["https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld","http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld"] } diff --git a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventTwoRoles.json b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventTwoRoles.json index f1244eeae..f0d0457f7 100644 --- a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventTwoRoles.json +++ b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventTwoRoles.json @@ -1,8 +1,9 @@ { "operationType":"ATTRIBUTE_APPEND", "entityId":"urn:ngsi-ld:Group:ab67edf3-238c-4f50-83f4-617c620c62eb", + "entityType": "Group", "attributeName": "roles", - "operationPayload":"{\"roles\":{\"type\":\"Property\",\"value\":[\"stellio-admin\", \"stellio-creator\"]}}", + "operationPayload":"{\"type\":\"Property\",\"value\":[\"stellio-admin\", \"stellio-creator\"]}", "updatedEntity": "", "contexts":["https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld","http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld"] } diff --git a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendToClient.json b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendToClient.json index 6d5080eeb..7bc36899a 100644 --- a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendToClient.json +++ b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendToClient.json @@ -1,8 +1,9 @@ { "operationType":"ATTRIBUTE_APPEND", "entityId":"urn:ngsi-ld:Client:ab67edf3-238c-4f50-83f4-617c620c62eb", + "entityType": "Client", "attributeName": "roles", - "operationPayload":"{\"serviceAccountId\":{\"type\":\"Property\",\"value\":\"a1eec755-c455-4f28-abe4-06558176fcc5\"},\"roles\":{\"type\":\"Property\",\"value\":[\"stellio-admin\"]}}", + "operationPayload":"{\"type\":\"Property\",\"value\":[\"stellio-admin\"]}", "updatedEntity": "", "contexts":["https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld","http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld"] } diff --git a/search-service/src/test/resources/ngsild/authorization/RightAddOnEntity.json b/search-service/src/test/resources/ngsild/authorization/RightAddOnEntity.json index dd24aedab..28f0afb86 100644 --- a/search-service/src/test/resources/ngsild/authorization/RightAddOnEntity.json +++ b/search-service/src/test/resources/ngsild/authorization/RightAddOnEntity.json @@ -1,5 +1,6 @@ { "entityId": "urn:ngsi-ld:User:312b30b4-9279-4f7e-bdc5-ec56d699bb7d", + "entityType": "User", "attributeName": "rCanRead", "operationPayload": "{\"type\":\"Relationship\",\"object\":\"urn:ngsi-ld:Beekeeper:01\"}", "updatedEntity": "", diff --git a/search-service/src/test/resources/ngsild/authorization/RightRemoveOnEntity.json b/search-service/src/test/resources/ngsild/authorization/RightRemoveOnEntity.json index da0265e34..524f27222 100644 --- a/search-service/src/test/resources/ngsild/authorization/RightRemoveOnEntity.json +++ b/search-service/src/test/resources/ngsild/authorization/RightRemoveOnEntity.json @@ -1,5 +1,6 @@ { "entityId": "urn:ngsi-ld:User:312b30b4-9279-4f7e-bdc5-ec56d699bb7d", + "entityType": "User", "attributeName": "urn:ngsi-ld:Beekeeper:01", "updatedEntity": "", "contexts": [ diff --git a/search-service/src/test/resources/ngsild/authorization/UserCreateEvent.json b/search-service/src/test/resources/ngsild/authorization/UserCreateEvent.json index e718e197f..e8ccd92ae 100644 --- a/search-service/src/test/resources/ngsild/authorization/UserCreateEvent.json +++ b/search-service/src/test/resources/ngsild/authorization/UserCreateEvent.json @@ -1,6 +1,7 @@ { "operationType": "ENTITY_CREATE", "entityId": "urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0", + "entityType": "User", "operationPayload": "{\"id\":\"urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0\",\"type\":\"User\",\"username\":{\"type\":\"Property\",\"value\":\"stellio\"}}", "contexts": [ "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld", diff --git a/search-service/src/test/resources/ngsild/authorization/UserDeleteEvent.json b/search-service/src/test/resources/ngsild/authorization/UserDeleteEvent.json index d8e1ffcc4..104f7663f 100644 --- a/search-service/src/test/resources/ngsild/authorization/UserDeleteEvent.json +++ b/search-service/src/test/resources/ngsild/authorization/UserDeleteEvent.json @@ -1,6 +1,7 @@ { "operationType": "ENTITY_DELETE", "entityId": "urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0", + "entityType": "User", "contexts": [ "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld", "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/web/AuthUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/web/AuthUtils.kt index b87604746..d6c3117b2 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/web/AuthUtils.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/web/AuthUtils.kt @@ -5,6 +5,7 @@ import org.springframework.security.core.context.SecurityContextImpl import org.springframework.security.oauth2.jwt.Jwt import reactor.core.publisher.Mono import java.net.URI +import java.util.Locale import java.util.UUID fun extractSubjectOrEmpty(): Mono { @@ -21,7 +22,7 @@ fun String.toUUID(): UUID = fun getSubjectTypeFromSubjectId(subjectId: URI): SubjectType { val type = subjectId.toString().split(":")[2] - return SubjectType.valueOf(type.toUpperCase()) + return SubjectType.valueOf(type.uppercase(Locale.getDefault())) } enum class SubjectType { From ba77105866f5c21ee012ab29501a481232c99ac0 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Mon, 29 Nov 2021 15:50:20 +0100 Subject: [PATCH 11/28] refactor: integrate entity access handler with common event service --- .../authorization/AuthorizationService.kt | 2 + .../entity/service/EntityEventService.kt | 10 ++- .../entity/web/EntityAccessControlHandler.kt | 42 ++++------- .../web/EntityAccessControlHandlerTests.kt | 73 ++++++------------- 4 files changed, 47 insertions(+), 80 deletions(-) diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt index 25e11bf22..6555b8b06 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt @@ -8,7 +8,9 @@ interface AuthorizationService { const val USER_PREFIX: String = "urn:ngsi-ld:User:" const val AUTHORIZATION_ONTOLOGY = "https://ontology.eglobalmark.com/authorization#" const val USER_LABEL = AUTHORIZATION_ONTOLOGY + "User" + const val GROUP_LABEL = AUTHORIZATION_ONTOLOGY + "Group" const val CLIENT_LABEL = AUTHORIZATION_ONTOLOGY + "Client" + val IAM_LABELS = listOf(USER_LABEL, GROUP_LABEL, CLIENT_LABEL) const val EGM_ROLES = AUTHORIZATION_ONTOLOGY + "roles" const val R_CAN_READ = AUTHORIZATION_ONTOLOGY + "rCanRead" const val R_CAN_WRITE = AUTHORIZATION_ONTOLOGY + "rCanWrite" diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/service/EntityEventService.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/service/EntityEventService.kt index 815ff4d16..2963491a0 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/service/EntityEventService.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/service/EntityEventService.kt @@ -3,6 +3,7 @@ package com.egm.stellio.entity.service import arrow.core.Validated import arrow.core.invalid import arrow.core.valid +import com.egm.stellio.entity.authorization.AuthorizationService import com.egm.stellio.entity.model.UpdateOperationResult import com.egm.stellio.entity.model.UpdateResult import com.egm.stellio.entity.model.UpdatedDetails @@ -39,6 +40,8 @@ class EntityEventService( private val logger = LoggerFactory.getLogger(javaClass) + private val iamTopic = "cim.iam.rights" + internal fun composeTopicName(entityType: String): Validated { val topicName = entityChannelName(entityType) return try { @@ -62,8 +65,11 @@ class EntityEventService( } ) - private fun entityChannelName(channelSuffix: String) = - "cim.entity.$channelSuffix" + private fun entityChannelName(entityType: String) = + if (AuthorizationService.IAM_LABELS.contains(entityType)) + iamTopic + else + "cim.entity.$entityType" @Async fun publishEntityCreateEvent( diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt index c1f6dd82a..18e90a644 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt @@ -1,16 +1,15 @@ package com.egm.stellio.entity.web import com.egm.stellio.entity.authorization.AuthorizationService +import com.egm.stellio.entity.model.updateResultFromDetailedResult +import com.egm.stellio.entity.service.EntityEventService import com.egm.stellio.entity.service.EntityService -import com.egm.stellio.shared.model.AttributeAppendEvent -import com.egm.stellio.shared.model.AttributeDeleteEvent import com.egm.stellio.shared.model.NgsiLdRelationship import com.egm.stellio.shared.model.NgsiLdRelationshipInstance import com.egm.stellio.shared.model.ResourceNotFoundException import com.egm.stellio.shared.model.parseToNgsiLdAttributes import com.egm.stellio.shared.util.JSON_LD_CONTENT_TYPE import com.egm.stellio.shared.util.JsonLdUtils -import com.egm.stellio.shared.util.JsonUtils import com.egm.stellio.shared.util.checkAndGetContext import com.egm.stellio.shared.util.getContextFromLinkHeaderOrDefault import com.egm.stellio.shared.util.toUri @@ -20,7 +19,6 @@ import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity -import org.springframework.kafka.core.KafkaTemplate import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping @@ -35,7 +33,7 @@ import reactor.core.publisher.Mono class EntityAccessControlHandler( private val entityService: EntityService, private val authorizationService: AuthorizationService, - private val kafkaTemplate: KafkaTemplate + private val entityEventService: EntityEventService ) { @PostMapping("/{subjectId}/attrs", consumes = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) @@ -61,7 +59,7 @@ class EntityAccessControlHandler( authorizationService.userIsAdminOfEntity(targetEntityId, userId) } - authorizedInstances.forEach { + val results = authorizedInstances.map { val ngsiLdRelationship = it.first as NgsiLdRelationship entityService.appendEntityRelationship( subjectId.toUri(), @@ -69,25 +67,15 @@ class EntityAccessControlHandler( it.second, false ) - - val operationPayload = mapOf( - "type" to "Relationship", - "object" to it.second.objectId, - "datasetId" to it.second.datasetId - ) - val attributeAppendEvent = AttributeAppendEvent( - entityId = subjectId.toUri(), - // TODO costly and not exactly what we want - entityType = entityService.getEntityCoreProperties(subjectId.toUri()).type[0], - attributeName = ngsiLdRelationship.compactName, - datasetId = it.second.datasetId, - operationPayload = JsonUtils.serializeObject(operationPayload), - updatedEntity = "", - contexts = contexts - ) - kafkaTemplate.send("cim.iam.rights", subjectId, JsonUtils.serializeObject(attributeAppendEvent)) } + entityEventService.publishAttributeAppendEvents( + subjectId.toUri(), + jsonLdAttributes, + updateResultFromDetailedResult(results), + contexts + ) + return if (unauthorizedInstances.isEmpty()) ResponseEntity.status(HttpStatus.NO_CONTENT).build() else { @@ -120,16 +108,12 @@ class EntityAccessControlHandler( val removeResult = authorizationService.removeUserRightsOnEntity(entityId.toUri(), subjectId.toUri()) if (removeResult == 1) { - val attributeAppendEvent = AttributeDeleteEvent( + entityEventService.publishAttributeDeleteEvent( entityId = subjectId.toUri(), - // TODO costly and not exactly what we want - entityType = entityService.getEntityCoreProperties(subjectId.toUri()).type[0], attributeName = entityId, - datasetId = null, - updatedEntity = "", + deleteAll = false, contexts = contexts ) - kafkaTemplate.send("cim.iam.rights", subjectId, JsonUtils.serializeObject(attributeAppendEvent)) } return if (removeResult == 1) diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt index 8e23016d6..86652ffaf 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt @@ -4,22 +4,20 @@ import com.egm.stellio.entity.authorization.AuthorizationService import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_READ import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_WRITE import com.egm.stellio.entity.config.WebSecurityTestConfig -import com.egm.stellio.entity.model.Entity +import com.egm.stellio.entity.model.UpdateAttributeResult +import com.egm.stellio.entity.model.UpdateOperationResult +import com.egm.stellio.entity.service.EntityEventService import com.egm.stellio.entity.service.EntityService import com.egm.stellio.shared.WithMockCustomUser -import com.egm.stellio.shared.model.AttributeAppendEvent -import com.egm.stellio.shared.model.AttributeDeleteEvent -import com.egm.stellio.shared.model.EventsType import com.egm.stellio.shared.util.JSON_LD_MEDIA_TYPE import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CORE_CONTEXT import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_EGM_AUTHORIZATION_CONTEXT -import com.egm.stellio.shared.util.JsonUtils import com.egm.stellio.shared.util.buildContextLinkHeader -import com.egm.stellio.shared.util.matchContent import com.egm.stellio.shared.util.toUri import com.ninjasquad.springmockk.MockkBean +import io.mockk.Runs import io.mockk.every -import io.mockk.mockk +import io.mockk.just import io.mockk.verify import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test @@ -28,10 +26,8 @@ import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest import org.springframework.context.annotation.Import import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus -import org.springframework.kafka.core.KafkaTemplate import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.reactive.server.WebTestClient -import org.springframework.util.concurrent.SettableListenableFuture @ActiveProfiles("test") @WebFluxTest(EntityAccessControlHandler::class) @@ -49,11 +45,10 @@ class EntityAccessControlHandlerTests { private lateinit var authorizationService: AuthorizationService @MockkBean(relaxed = true) - private lateinit var kafkaTemplate: KafkaTemplate + private lateinit var entityEventService: EntityEventService private val subjectId = "urn:ngsi-ld:User:0123".toUri() private val entityUri1 = "urn:ngsi-ld:Entity:entityId1".toUri() - private val entityType = "https://my.context/Entity" @BeforeAll fun configureWebClientDefaults() { @@ -67,7 +62,6 @@ class EntityAccessControlHandlerTests { @Test fun `it should allow an authorized user to give access to an entity`() { - val entity = mockk() val requestPayload = """ { @@ -80,9 +74,10 @@ class EntityAccessControlHandlerTests { """.trimIndent() every { authorizationService.userIsAdminOfEntity(any(), any()) } returns true - every { entityService.getEntityCoreProperties(any()) } returns entity - every { entity.type } returns listOf(entityType) - every { kafkaTemplate.send(any(), any(), any()) } returns SettableListenableFuture() + every { + entityService.appendEntityRelationship(any(), any(), any(), any()) + } returns UpdateAttributeResult(R_CAN_READ, null, UpdateOperationResult.APPENDED) + every { entityEventService.publishAttributeAppendEvents(any(), any(), any(), any()) } just Runs webClient.post() .uri("/ngsi-ld/v1/entityAccessControl/$subjectId/attrs") @@ -110,27 +105,20 @@ class EntityAccessControlHandlerTests { } verify { - kafkaTemplate.send( - eq("cim.iam.rights"), - eq(subjectId.toString()), + entityEventService.publishAttributeAppendEvents( + eq(subjectId), + any(), match { - val event = JsonUtils.deserializeAs(it) - val expectedOperationPayload = - "{\"type\":\"Relationship\",\"object\":\"urn:ngsi-ld:Entity:entityId1\"}" - event.entityId == subjectId && - event.attributeName == "rCanRead" && - event.operationType == EventsType.ATTRIBUTE_APPEND && - event.contexts.size == 2 && - event.datasetId == null && - event.operationPayload.matchContent(expectedOperationPayload) - } + it.updated.size == 1 && + it.updated[0].updateOperationResult == UpdateOperationResult.APPENDED + }, + eq(listOf(NGSILD_EGM_AUTHORIZATION_CONTEXT, NGSILD_CORE_CONTEXT)) ) } } @Test fun `it should allow an authorized user to give access to a set of entities`() { - val entity = mockk() val entityUri2 = "urn:ngsi-ld:Entity:entityId2".toUri() val entityUri3 = "urn:ngsi-ld:Entity:entityId3".toUri() val requestPayload = @@ -155,8 +143,6 @@ class EntityAccessControlHandlerTests { """.trimIndent() every { authorizationService.userIsAdminOfEntity(any(), any()) } returns true - every { entityService.getEntityCoreProperties(any()) } returns entity - every { entity.type } returns listOf(entityType) webClient.post() .uri("/ngsi-ld/v1/entityAccessControl/$subjectId/attrs") @@ -195,7 +181,6 @@ class EntityAccessControlHandlerTests { @Test fun `it should only allow to give access to authorized entities`() { - val entity = mockk() val entityUri2 = "urn:ngsi-ld:Entity:entityId2".toUri() val requestPayload = """ @@ -216,8 +201,6 @@ class EntityAccessControlHandlerTests { every { authorizationService.userIsAdminOfEntity(eq(entityUri1), any()) } returns true every { authorizationService.userIsAdminOfEntity(eq(entityUri2), any()) } returns false - every { entityService.getEntityCoreProperties(any()) } returns entity - every { entity.type } returns listOf(entityType) webClient.post() .uri("/ngsi-ld/v1/entityAccessControl/$subjectId/attrs") @@ -242,13 +225,9 @@ class EntityAccessControlHandlerTests { @Test fun `it should allow an authorized user to remove access to an entity`() { - val entity = mockk() - every { authorizationService.userIsAdminOfEntity(any(), any()) } returns true every { authorizationService.removeUserRightsOnEntity(any(), any()) } returns 1 - every { entityService.getEntityCoreProperties(any()) } returns entity - every { entity.type } returns listOf(entityType) - every { kafkaTemplate.send(any(), any(), any()) } returns SettableListenableFuture() + every { entityEventService.publishAttributeDeleteEvent(any(), any(), any(), any(), any()) } just Runs webClient.delete() .uri("/ngsi-ld/v1/entityAccessControl/$subjectId/attrs/$entityUri1") @@ -268,16 +247,12 @@ class EntityAccessControlHandlerTests { } verify { - kafkaTemplate.send( - eq("cim.iam.rights"), - eq(subjectId.toString()), - match { - val event = JsonUtils.deserializeAs(it) - event.entityId == subjectId && - event.attributeName == entityUri1.toString() && - event.operationType == EventsType.ATTRIBUTE_DELETE && - event.contexts.size == 1 - } + entityEventService.publishAttributeDeleteEvent( + eq(subjectId), + eq(entityUri1.toString()), + null, + eq(false), + eq(listOf(NGSILD_EGM_AUTHORIZATION_CONTEXT)) ) } } From 293b5c12fc05523d4be96111ded424639142b0b6 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Mon, 29 Nov 2021 18:55:27 +0100 Subject: [PATCH 12/28] refactor: put common sample tests payloads in shared testFixtures --- .../service/SubscriptionEventListenerTests.kt | 12 ++++++++---- .../subscriptions/notificationCreateEvent.jsonld | 10 ---------- .../stellio/search/service/IAMListenerTests.kt | 16 ++++++++-------- .../SubscriptionEventListenerServiceTest.kt | 10 +++++----- .../GroupMembershipAppendEvent.json | 13 ------------- .../GroupMembershipDeleteEvent.json | 12 ------------ .../ngsild/authorization/GroupUpdateEvent.json | 12 ------------ .../RealmRoleAppendEventNoRole.json | 9 --------- .../RealmRoleAppendEventOneRole.json | 9 --------- .../RealmRoleAppendEventTwoRoles.json | 9 --------- .../authorization/RealmRoleAppendToClient.json | 9 --------- .../ngsild/authorization/UserCreateEvent.json | 10 ---------- .../ngsild/authorization/UserDeleteEvent.json | 9 --------- .../listened/subscriptionCreateEvent.jsonld | 10 ---------- .../listened/subscriptionDeleteEvent.jsonld | 9 --------- .../events/authorization/ClientCreateEvent.json | 0 .../events/authorization/ClientDeleteEvent.json | 0 .../events/authorization/GroupCreateEvent.json | 0 .../events/authorization/GroupDeleteEvent.json | 0 .../GroupMembershipAppendEvent.json | 0 .../GroupMembershipDeleteEvent.json | 0 .../events/authorization/GroupUpdateEvent.json | 0 .../RealmRoleAppendEventNoRole.json | 0 .../RealmRoleAppendEventOneRole.json | 0 .../RealmRoleAppendEventTwoRoles.json | 0 .../authorization/RealmRoleAppendToClient.json | 0 .../events}/authorization/RightAddOnEntity.json | 0 .../authorization/RightRemoveOnEntity.json | 0 .../events/authorization/UserCreateEvent.json | 0 .../events/authorization/UserDeleteEvent.json | 0 .../subscription}/notificationCreateEvent.jsonld | 4 ++-- .../subscription}/subscriptionCreateEvent.jsonld | 0 .../subscription}/subscriptionDeleteEvent.jsonld | 0 33 files changed, 23 insertions(+), 140 deletions(-) delete mode 100644 entity-service/src/test/resources/ngsild/events/subscriptions/notificationCreateEvent.jsonld delete mode 100644 search-service/src/test/resources/ngsild/authorization/GroupMembershipAppendEvent.json delete mode 100644 search-service/src/test/resources/ngsild/authorization/GroupMembershipDeleteEvent.json delete mode 100644 search-service/src/test/resources/ngsild/authorization/GroupUpdateEvent.json delete mode 100644 search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventNoRole.json delete mode 100644 search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventOneRole.json delete mode 100644 search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventTwoRoles.json delete mode 100644 search-service/src/test/resources/ngsild/authorization/RealmRoleAppendToClient.json delete mode 100644 search-service/src/test/resources/ngsild/authorization/UserCreateEvent.json delete mode 100644 search-service/src/test/resources/ngsild/authorization/UserDeleteEvent.json delete mode 100644 search-service/src/test/resources/ngsild/events/listened/subscriptionCreateEvent.jsonld delete mode 100644 search-service/src/test/resources/ngsild/events/listened/subscriptionDeleteEvent.jsonld rename {entity-service/src/test => shared/src/testFixtures}/resources/ngsild/events/authorization/ClientCreateEvent.json (100%) rename {entity-service/src/test => shared/src/testFixtures}/resources/ngsild/events/authorization/ClientDeleteEvent.json (100%) rename {entity-service/src/test => shared/src/testFixtures}/resources/ngsild/events/authorization/GroupCreateEvent.json (100%) rename {entity-service/src/test => shared/src/testFixtures}/resources/ngsild/events/authorization/GroupDeleteEvent.json (100%) rename {entity-service/src/test => shared/src/testFixtures}/resources/ngsild/events/authorization/GroupMembershipAppendEvent.json (100%) rename {entity-service/src/test => shared/src/testFixtures}/resources/ngsild/events/authorization/GroupMembershipDeleteEvent.json (100%) rename {entity-service/src/test => shared/src/testFixtures}/resources/ngsild/events/authorization/GroupUpdateEvent.json (100%) rename {entity-service/src/test => shared/src/testFixtures}/resources/ngsild/events/authorization/RealmRoleAppendEventNoRole.json (100%) rename {entity-service/src/test => shared/src/testFixtures}/resources/ngsild/events/authorization/RealmRoleAppendEventOneRole.json (100%) rename {entity-service/src/test => shared/src/testFixtures}/resources/ngsild/events/authorization/RealmRoleAppendEventTwoRoles.json (100%) rename {entity-service/src/test => shared/src/testFixtures}/resources/ngsild/events/authorization/RealmRoleAppendToClient.json (100%) rename {search-service/src/test/resources/ngsild => shared/src/testFixtures/resources/ngsild/events}/authorization/RightAddOnEntity.json (100%) rename {search-service/src/test/resources/ngsild => shared/src/testFixtures/resources/ngsild/events}/authorization/RightRemoveOnEntity.json (100%) rename {entity-service/src/test => shared/src/testFixtures}/resources/ngsild/events/authorization/UserCreateEvent.json (100%) rename {entity-service/src/test => shared/src/testFixtures}/resources/ngsild/events/authorization/UserDeleteEvent.json (100%) rename {search-service/src/test/resources/ngsild/events/listened => shared/src/testFixtures/resources/ngsild/events/subscription}/notificationCreateEvent.jsonld (87%) rename {entity-service/src/test/resources/ngsild/events/subscriptions => shared/src/testFixtures/resources/ngsild/events/subscription}/subscriptionCreateEvent.jsonld (100%) rename {entity-service/src/test/resources/ngsild/events/subscriptions => shared/src/testFixtures/resources/ngsild/events/subscription}/subscriptionDeleteEvent.jsonld (100%) diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/service/SubscriptionEventListenerTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/service/SubscriptionEventListenerTests.kt index 63883d232..02b4dab8d 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/service/SubscriptionEventListenerTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/service/SubscriptionEventListenerTests.kt @@ -26,7 +26,7 @@ class SubscriptionEventListenerTests { @Test fun `it should parse and create subscription entity`() { val subscription = - loadSampleData("events/subscriptions/subscriptionCreateEvent.jsonld") + loadSampleData("events/subscription/subscriptionCreateEvent.jsonld") every { subscriptionHandlerService.createSubscriptionEntity(any(), any(), any()) } just Runs @@ -45,7 +45,7 @@ class SubscriptionEventListenerTests { @Test fun `it should delete a subscription entity`() { val subscriptionEvent = - loadSampleData("events/subscriptions/subscriptionDeleteEvent.jsonld") + loadSampleData("events/subscription/subscriptionDeleteEvent.jsonld") every { subscriptionHandlerService.deleteSubscriptionEntity(any()) } just Runs @@ -63,7 +63,7 @@ class SubscriptionEventListenerTests { @Test fun `it should parse and create notification entity`() { val notification = - loadSampleData("events/subscriptions/notificationCreateEvent.jsonld") + loadSampleData("events/subscription/notificationCreateEvent.jsonld") every { subscriptionHandlerService.createNotificationEntity(any(), any(), any(), any()) } just Runs @@ -74,7 +74,11 @@ class SubscriptionEventListenerTests { "urn:ngsi-ld:Notification:1234".toUri(), "Notification", "urn:ngsi-ld:Subscription:1234".toUri(), - mapOf("notifiedAt" to "2020-03-10T00:00:00Z") + match { + it.containsKey("notifiedAt") && + it["notifiedAt"] == "2020-03-10T00:00:00Z" && + it.containsKey("data") + } ) } confirmVerified(subscriptionHandlerService) diff --git a/entity-service/src/test/resources/ngsild/events/subscriptions/notificationCreateEvent.jsonld b/entity-service/src/test/resources/ngsild/events/subscriptions/notificationCreateEvent.jsonld deleted file mode 100644 index 4ccbeaded..000000000 --- a/entity-service/src/test/resources/ngsild/events/subscriptions/notificationCreateEvent.jsonld +++ /dev/null @@ -1,10 +0,0 @@ -{ - "entityId":"urn:ngsi-ld:Notification:1234", - "entityType": "Notification", - "operationPayload": "{\"id\": \"urn:ngsi-ld:Notification:1234\",\"type\": \"Notification\",\"notifiedAt\": \"2020-03-10T00:00:00Z\",\"subscriptionId\": \"urn:ngsi-ld:Subscription:1234\"}", - "operationType":"ENTITY_CREATE", - "contexts":[ - "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/shared-jsonld-contexts/egm.jsonld", - "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" - ] -} diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt index ef764b706..4423bbc28 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt @@ -24,7 +24,7 @@ class IAMListenerTests { @Test fun `it should handle a create event for a subject`() { - val subjectCreateEvent = loadSampleData("authorization/UserCreateEvent.json") + val subjectCreateEvent = loadSampleData("events/authorization/UserCreateEvent.json") iamListener.processMessage(subjectCreateEvent) @@ -44,7 +44,7 @@ class IAMListenerTests { @Test fun `it should handle a delete event for a subject`() { - val subjectDeleteEvent = loadSampleData("authorization/UserDeleteEvent.json") + val subjectDeleteEvent = loadSampleData("events/authorization/UserDeleteEvent.json") iamListener.processMessage(subjectDeleteEvent) @@ -60,7 +60,7 @@ class IAMListenerTests { @Test fun `it should handle an append event adding a stellio-admin role for a group`() { - val roleAppendEvent = loadSampleData("authorization/RealmRoleAppendEventOneRole.json") + val roleAppendEvent = loadSampleData("events/authorization/RealmRoleAppendEventOneRole.json") iamListener.processMessage(roleAppendEvent) @@ -76,7 +76,7 @@ class IAMListenerTests { @Test fun `it should handle an append event adding a stellio-admin role for a client`() { - val roleAppendEvent = loadSampleData("authorization/RealmRoleAppendToClient.json") + val roleAppendEvent = loadSampleData("events/authorization/RealmRoleAppendToClient.json") iamListener.processMessage(roleAppendEvent) @@ -92,7 +92,7 @@ class IAMListenerTests { @Test fun `it should handle an append event adding a stellio-admin role within two roles`() { - val roleAppendEvent = loadSampleData("authorization/RealmRoleAppendEventTwoRoles.json") + val roleAppendEvent = loadSampleData("events/authorization/RealmRoleAppendEventTwoRoles.json") iamListener.processMessage(roleAppendEvent) @@ -108,7 +108,7 @@ class IAMListenerTests { @Test fun `it should handle an append event removing a stellio-admin role for a group`() { - val roleAppendEvent = loadSampleData("authorization/RealmRoleAppendEventNoRole.json") + val roleAppendEvent = loadSampleData("events/authorization/RealmRoleAppendEventNoRole.json") iamListener.processMessage(roleAppendEvent) @@ -124,7 +124,7 @@ class IAMListenerTests { @Test fun `it should handle an append event adding a right on an entity`() { - val rightAppendEvent = loadSampleData("authorization/RightAddOnEntity.json") + val rightAppendEvent = loadSampleData("events/authorization/RightAddOnEntity.json") iamListener.processIamRights(rightAppendEvent) @@ -139,7 +139,7 @@ class IAMListenerTests { @Test fun `it should handle an delete event removing a right on an entity`() { - val rightRemoveEvent = loadSampleData("authorization/RightRemoveOnEntity.json") + val rightRemoveEvent = loadSampleData("events/authorization/RightRemoveOnEntity.json") iamListener.processIamRights(rightRemoveEvent) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubscriptionEventListenerServiceTest.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubscriptionEventListenerServiceTest.kt index 7a6c306e9..46d0a3224 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubscriptionEventListenerServiceTest.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubscriptionEventListenerServiceTest.kt @@ -30,7 +30,7 @@ class SubscriptionEventListenerServiceTest { @Test fun `it should parse a subscription and create a temporal entity reference`() { - val subscriptionEvent = loadSampleData("events/listened/subscriptionCreateEvent.jsonld") + val subscriptionEvent = loadSampleData("events/subscription/subscriptionCreateEvent.jsonld") every { temporalEntityAttributeService.create(any()) } returns Mono.just(1) @@ -41,7 +41,7 @@ class SubscriptionEventListenerServiceTest { match { entityTemporalProperty -> entityTemporalProperty.attributeName == "https://uri.etsi.org/ngsi-ld/notification" && entityTemporalProperty.attributeValueType == TemporalEntityAttribute.AttributeValueType.ANY && - entityTemporalProperty.entityId == "urn:ngsi-ld:Subscription:1234".toUri() && + entityTemporalProperty.entityId == "urn:ngsi-ld:Subscription:04".toUri() && entityTemporalProperty.type == "https://uri.etsi.org/ngsi-ld/Subscription" } ) @@ -52,7 +52,7 @@ class SubscriptionEventListenerServiceTest { @Test fun `it should parse a notification and create one related observation`() { val temporalEntityAttributeUuid = UUID.randomUUID() - val notificationEvent = loadSampleData("events/listened/notificationCreateEvent.jsonld") + val notificationEvent = loadSampleData("events/subscription/notificationCreateEvent.jsonld") every { temporalEntityAttributeService.getFirstForEntity(any()) } returns Mono.just(temporalEntityAttributeUuid) every { attributeInstanceService.create(any()) } returns Mono.just(1) @@ -84,14 +84,14 @@ class SubscriptionEventListenerServiceTest { @Test fun `it should delete subscription temporal references`() { - val subscriptionEvent = loadSampleData("events/listened/subscriptionDeleteEvent.jsonld") + val subscriptionEvent = loadSampleData("events/subscription/subscriptionDeleteEvent.jsonld") every { temporalEntityAttributeService.deleteTemporalEntityReferences(any()) } returns Mono.just(10) subscriptionEventListenerService.processSubscription(subscriptionEvent) verify { - temporalEntityAttributeService.deleteTemporalEntityReferences(eq("urn:ngsi-ld:Subscription:1234".toUri())) + temporalEntityAttributeService.deleteTemporalEntityReferences(eq("urn:ngsi-ld:Subscription:04".toUri())) } confirmVerified(temporalEntityAttributeService) diff --git a/search-service/src/test/resources/ngsild/authorization/GroupMembershipAppendEvent.json b/search-service/src/test/resources/ngsild/authorization/GroupMembershipAppendEvent.json deleted file mode 100644 index f5b3f5756..000000000 --- a/search-service/src/test/resources/ngsild/authorization/GroupMembershipAppendEvent.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "operationType": "ATTRIBUTE_APPEND", - "entityId": "urn:ngsi-ld:User:96e1f1e9-d798-48d7-820e-59f5a9a2abf5", - "entityType": "User", - "attributeName": "isMemberOf", - "datasetId": "urn:ngsi-ld:Dataset:7cdad168-96ee-4649-b768-a060ac2ef435", - "operationPayload": "{\"type\":\"Relationship\",\"datasetId\":\"urn:ngsi-ld:Dataset:7cdad168-96ee-4649-b768-a060ac2ef435\",\"object\":\"urn:ngsi-ld:Group:7cdad168-96ee-4649-b768-a060ac2ef435\"}", - "updatedEntity": "", - "contexts": [ - "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld", - "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" - ] -} diff --git a/search-service/src/test/resources/ngsild/authorization/GroupMembershipDeleteEvent.json b/search-service/src/test/resources/ngsild/authorization/GroupMembershipDeleteEvent.json deleted file mode 100644 index 6ab86551f..000000000 --- a/search-service/src/test/resources/ngsild/authorization/GroupMembershipDeleteEvent.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "operationType": "ATTRIBUTE_DELETE", - "entityId": "urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0", - "entityType": "User", - "attributeName": "isMemberOf", - "datasetId": "urn:ngsi-ld:Dataset:isMemberOf:7cdad168-96ee-4649-b768-a060ac2ef435", - "updatedEntity": "", - "contexts": [ - "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld", - "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" - ] -} diff --git a/search-service/src/test/resources/ngsild/authorization/GroupUpdateEvent.json b/search-service/src/test/resources/ngsild/authorization/GroupUpdateEvent.json deleted file mode 100644 index 7f2148214..000000000 --- a/search-service/src/test/resources/ngsild/authorization/GroupUpdateEvent.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "operationType": "ATTRIBUTE_REPLACE", - "entityId": "urn:ngsi-ld:Group:ab67edf3-238c-4f50-83f4-617c620c62eb", - "entityType": "Group", - "attributeName": "name", - "operationPayload": "{\"type\":\"Property\",\"value\":\"EGM Team\"}", - "updatedEntity": "", - "contexts": [ - "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld", - "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" - ] -} diff --git a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventNoRole.json b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventNoRole.json deleted file mode 100644 index 9f7db5502..000000000 --- a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventNoRole.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "operationType":"ATTRIBUTE_APPEND", - "entityId":"urn:ngsi-ld:Group:ab67edf3-238c-4f50-83f4-617c620c62eb", - "entityType": "Group", - "attributeName": "roles", - "operationPayload":"{\"type\":\"Property\",\"value\":[]}", - "updatedEntity": "", - "contexts":["https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld","http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld"] -} diff --git a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventOneRole.json b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventOneRole.json deleted file mode 100644 index 1ef294c34..000000000 --- a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventOneRole.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "operationType":"ATTRIBUTE_APPEND", - "entityId":"urn:ngsi-ld:Group:ab67edf3-238c-4f50-83f4-617c620c62eb", - "entityType": "Group", - "attributeName": "roles", - "operationPayload":"{\"type\":\"Property\",\"value\":[\"stellio-admin\"]}", - "updatedEntity": "", - "contexts":["https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld","http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld"] -} diff --git a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventTwoRoles.json b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventTwoRoles.json deleted file mode 100644 index f0d0457f7..000000000 --- a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendEventTwoRoles.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "operationType":"ATTRIBUTE_APPEND", - "entityId":"urn:ngsi-ld:Group:ab67edf3-238c-4f50-83f4-617c620c62eb", - "entityType": "Group", - "attributeName": "roles", - "operationPayload":"{\"type\":\"Property\",\"value\":[\"stellio-admin\", \"stellio-creator\"]}", - "updatedEntity": "", - "contexts":["https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld","http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld"] -} diff --git a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendToClient.json b/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendToClient.json deleted file mode 100644 index 7bc36899a..000000000 --- a/search-service/src/test/resources/ngsild/authorization/RealmRoleAppendToClient.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "operationType":"ATTRIBUTE_APPEND", - "entityId":"urn:ngsi-ld:Client:ab67edf3-238c-4f50-83f4-617c620c62eb", - "entityType": "Client", - "attributeName": "roles", - "operationPayload":"{\"type\":\"Property\",\"value\":[\"stellio-admin\"]}", - "updatedEntity": "", - "contexts":["https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld","http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld"] -} diff --git a/search-service/src/test/resources/ngsild/authorization/UserCreateEvent.json b/search-service/src/test/resources/ngsild/authorization/UserCreateEvent.json deleted file mode 100644 index e8ccd92ae..000000000 --- a/search-service/src/test/resources/ngsild/authorization/UserCreateEvent.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "operationType": "ENTITY_CREATE", - "entityId": "urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0", - "entityType": "User", - "operationPayload": "{\"id\":\"urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0\",\"type\":\"User\",\"username\":{\"type\":\"Property\",\"value\":\"stellio\"}}", - "contexts": [ - "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld", - "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" - ] -} diff --git a/search-service/src/test/resources/ngsild/authorization/UserDeleteEvent.json b/search-service/src/test/resources/ngsild/authorization/UserDeleteEvent.json deleted file mode 100644 index 104f7663f..000000000 --- a/search-service/src/test/resources/ngsild/authorization/UserDeleteEvent.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "operationType": "ENTITY_DELETE", - "entityId": "urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0", - "entityType": "User", - "contexts": [ - "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld", - "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" - ] -} diff --git a/search-service/src/test/resources/ngsild/events/listened/subscriptionCreateEvent.jsonld b/search-service/src/test/resources/ngsild/events/listened/subscriptionCreateEvent.jsonld deleted file mode 100644 index 44c5f3efe..000000000 --- a/search-service/src/test/resources/ngsild/events/listened/subscriptionCreateEvent.jsonld +++ /dev/null @@ -1,10 +0,0 @@ -{ - "operationType": "ENTITY_CREATE", - "entityId": "urn:ngsi-ld:Subscription:1234", - "entityType": "Subscription", - "operationPayload": "{\"id\": \"urn:ngsi-ld:Subscription:1234\",\"type\": \"Subscription\",\"name\": \"Alerte frelons\",\"description\": \"Description de mon alerte frelons\",\"entities\": [{\"type\": \"https://ontology.eglobalmark.com/apic#BeeHive\"}]}", - "contexts":[ - "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/shared-jsonld-contexts/egm.jsonld", - "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" - ] -} diff --git a/search-service/src/test/resources/ngsild/events/listened/subscriptionDeleteEvent.jsonld b/search-service/src/test/resources/ngsild/events/listened/subscriptionDeleteEvent.jsonld deleted file mode 100644 index 7d1d97dec..000000000 --- a/search-service/src/test/resources/ngsild/events/listened/subscriptionDeleteEvent.jsonld +++ /dev/null @@ -1,9 +0,0 @@ -{ - "operationType": "ENTITY_DELETE", - "entityId": "urn:ngsi-ld:Subscription:1234", - "entityType": "Subscription", - "contexts":[ - "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/shared-jsonld-contexts/egm.jsonld", - "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" - ] -} diff --git a/entity-service/src/test/resources/ngsild/events/authorization/ClientCreateEvent.json b/shared/src/testFixtures/resources/ngsild/events/authorization/ClientCreateEvent.json similarity index 100% rename from entity-service/src/test/resources/ngsild/events/authorization/ClientCreateEvent.json rename to shared/src/testFixtures/resources/ngsild/events/authorization/ClientCreateEvent.json diff --git a/entity-service/src/test/resources/ngsild/events/authorization/ClientDeleteEvent.json b/shared/src/testFixtures/resources/ngsild/events/authorization/ClientDeleteEvent.json similarity index 100% rename from entity-service/src/test/resources/ngsild/events/authorization/ClientDeleteEvent.json rename to shared/src/testFixtures/resources/ngsild/events/authorization/ClientDeleteEvent.json diff --git a/entity-service/src/test/resources/ngsild/events/authorization/GroupCreateEvent.json b/shared/src/testFixtures/resources/ngsild/events/authorization/GroupCreateEvent.json similarity index 100% rename from entity-service/src/test/resources/ngsild/events/authorization/GroupCreateEvent.json rename to shared/src/testFixtures/resources/ngsild/events/authorization/GroupCreateEvent.json diff --git a/entity-service/src/test/resources/ngsild/events/authorization/GroupDeleteEvent.json b/shared/src/testFixtures/resources/ngsild/events/authorization/GroupDeleteEvent.json similarity index 100% rename from entity-service/src/test/resources/ngsild/events/authorization/GroupDeleteEvent.json rename to shared/src/testFixtures/resources/ngsild/events/authorization/GroupDeleteEvent.json diff --git a/entity-service/src/test/resources/ngsild/events/authorization/GroupMembershipAppendEvent.json b/shared/src/testFixtures/resources/ngsild/events/authorization/GroupMembershipAppendEvent.json similarity index 100% rename from entity-service/src/test/resources/ngsild/events/authorization/GroupMembershipAppendEvent.json rename to shared/src/testFixtures/resources/ngsild/events/authorization/GroupMembershipAppendEvent.json diff --git a/entity-service/src/test/resources/ngsild/events/authorization/GroupMembershipDeleteEvent.json b/shared/src/testFixtures/resources/ngsild/events/authorization/GroupMembershipDeleteEvent.json similarity index 100% rename from entity-service/src/test/resources/ngsild/events/authorization/GroupMembershipDeleteEvent.json rename to shared/src/testFixtures/resources/ngsild/events/authorization/GroupMembershipDeleteEvent.json diff --git a/entity-service/src/test/resources/ngsild/events/authorization/GroupUpdateEvent.json b/shared/src/testFixtures/resources/ngsild/events/authorization/GroupUpdateEvent.json similarity index 100% rename from entity-service/src/test/resources/ngsild/events/authorization/GroupUpdateEvent.json rename to shared/src/testFixtures/resources/ngsild/events/authorization/GroupUpdateEvent.json diff --git a/entity-service/src/test/resources/ngsild/events/authorization/RealmRoleAppendEventNoRole.json b/shared/src/testFixtures/resources/ngsild/events/authorization/RealmRoleAppendEventNoRole.json similarity index 100% rename from entity-service/src/test/resources/ngsild/events/authorization/RealmRoleAppendEventNoRole.json rename to shared/src/testFixtures/resources/ngsild/events/authorization/RealmRoleAppendEventNoRole.json diff --git a/entity-service/src/test/resources/ngsild/events/authorization/RealmRoleAppendEventOneRole.json b/shared/src/testFixtures/resources/ngsild/events/authorization/RealmRoleAppendEventOneRole.json similarity index 100% rename from entity-service/src/test/resources/ngsild/events/authorization/RealmRoleAppendEventOneRole.json rename to shared/src/testFixtures/resources/ngsild/events/authorization/RealmRoleAppendEventOneRole.json diff --git a/entity-service/src/test/resources/ngsild/events/authorization/RealmRoleAppendEventTwoRoles.json b/shared/src/testFixtures/resources/ngsild/events/authorization/RealmRoleAppendEventTwoRoles.json similarity index 100% rename from entity-service/src/test/resources/ngsild/events/authorization/RealmRoleAppendEventTwoRoles.json rename to shared/src/testFixtures/resources/ngsild/events/authorization/RealmRoleAppendEventTwoRoles.json diff --git a/entity-service/src/test/resources/ngsild/events/authorization/RealmRoleAppendToClient.json b/shared/src/testFixtures/resources/ngsild/events/authorization/RealmRoleAppendToClient.json similarity index 100% rename from entity-service/src/test/resources/ngsild/events/authorization/RealmRoleAppendToClient.json rename to shared/src/testFixtures/resources/ngsild/events/authorization/RealmRoleAppendToClient.json diff --git a/search-service/src/test/resources/ngsild/authorization/RightAddOnEntity.json b/shared/src/testFixtures/resources/ngsild/events/authorization/RightAddOnEntity.json similarity index 100% rename from search-service/src/test/resources/ngsild/authorization/RightAddOnEntity.json rename to shared/src/testFixtures/resources/ngsild/events/authorization/RightAddOnEntity.json diff --git a/search-service/src/test/resources/ngsild/authorization/RightRemoveOnEntity.json b/shared/src/testFixtures/resources/ngsild/events/authorization/RightRemoveOnEntity.json similarity index 100% rename from search-service/src/test/resources/ngsild/authorization/RightRemoveOnEntity.json rename to shared/src/testFixtures/resources/ngsild/events/authorization/RightRemoveOnEntity.json diff --git a/entity-service/src/test/resources/ngsild/events/authorization/UserCreateEvent.json b/shared/src/testFixtures/resources/ngsild/events/authorization/UserCreateEvent.json similarity index 100% rename from entity-service/src/test/resources/ngsild/events/authorization/UserCreateEvent.json rename to shared/src/testFixtures/resources/ngsild/events/authorization/UserCreateEvent.json diff --git a/entity-service/src/test/resources/ngsild/events/authorization/UserDeleteEvent.json b/shared/src/testFixtures/resources/ngsild/events/authorization/UserDeleteEvent.json similarity index 100% rename from entity-service/src/test/resources/ngsild/events/authorization/UserDeleteEvent.json rename to shared/src/testFixtures/resources/ngsild/events/authorization/UserDeleteEvent.json diff --git a/search-service/src/test/resources/ngsild/events/listened/notificationCreateEvent.jsonld b/shared/src/testFixtures/resources/ngsild/events/subscription/notificationCreateEvent.jsonld similarity index 87% rename from search-service/src/test/resources/ngsild/events/listened/notificationCreateEvent.jsonld rename to shared/src/testFixtures/resources/ngsild/events/subscription/notificationCreateEvent.jsonld index 3daf3f886..fc1e1ca41 100644 --- a/search-service/src/test/resources/ngsild/events/listened/notificationCreateEvent.jsonld +++ b/shared/src/testFixtures/resources/ngsild/events/subscription/notificationCreateEvent.jsonld @@ -4,7 +4,7 @@ "entityType": "Notification", "operationPayload": "{\"id\": \"urn:ngsi-ld:Notification:1234\",\"type\": \"Notification\",\"notifiedAt\": \"2020-03-10T00:00:00Z\",\"subscriptionId\": \"urn:ngsi-ld:Subscription:1234\",\"data\": [\n{\n\"id\": \"urn:ngsi-ld:BeeHive:TESTC\",\n\"type\": \"BeeHive\",\n\"createdAt\": \"2020-01-24T13:01:21.938Z\",\n\"name\": {\n\"type\": \"Property\",\n\"value\": \"ParisBeehive12\",\n\"createdAt\": \"2020-01-24T13:01:22.066Z\"\n},\n\"incoming\": {\n\"type\": \"Property\",\n\"value\": 1543,\n\"observedAt\": \"2020-01-24T13:01:22.066Z\"\n},\n\"@context\": [\n\"http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld\",\n\"https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/shared-jsonld-contexts/egm.jsonld\",\n\"https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/apic/jsonld-contexts/apic.jsonld\"\n]\n},\n{\n\"id\": \"urn:ngsi-ld:BeeHive:TESTD\",\n\"type\": \"BeeHive\",\n\"createdAt\": \"2020-02-24T13:01:21.938Z\",\n\"name\": {\n\"type\": \"Property\",\n\"value\": \"ValbonneBeehive12\",\n\"createdAt\": \"2020-02-24T13:01:22.066Z\"\n},\n\"incoming\": {\n\"type\": \"Property\",\n\"value\": 6688,\n\"observedAt\": \"2020-02-25T13:01:22.066Z\"\n}\n}\n]\n}", "contexts":[ - "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/shared-jsonld-contexts/egm.jsonld", - "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" + "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/shared-jsonld-contexts/egm.jsonld", + "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" ] } diff --git a/entity-service/src/test/resources/ngsild/events/subscriptions/subscriptionCreateEvent.jsonld b/shared/src/testFixtures/resources/ngsild/events/subscription/subscriptionCreateEvent.jsonld similarity index 100% rename from entity-service/src/test/resources/ngsild/events/subscriptions/subscriptionCreateEvent.jsonld rename to shared/src/testFixtures/resources/ngsild/events/subscription/subscriptionCreateEvent.jsonld diff --git a/entity-service/src/test/resources/ngsild/events/subscriptions/subscriptionDeleteEvent.jsonld b/shared/src/testFixtures/resources/ngsild/events/subscription/subscriptionDeleteEvent.jsonld similarity index 100% rename from entity-service/src/test/resources/ngsild/events/subscriptions/subscriptionDeleteEvent.jsonld rename to shared/src/testFixtures/resources/ngsild/events/subscription/subscriptionDeleteEvent.jsonld From 91a0320d5c47278bab7339dc3cff54bae4746a9c Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Wed, 1 Dec 2021 18:15:25 +0100 Subject: [PATCH 13/28] feat(entity): finalize endpoint to add rights - add checks on input date (type and name of attribute) - in case of 207, response has same format than other similar endpoints --- .../authorization/AuthorizationService.kt | 3 +- .../entity/web/EntityAccessControlHandler.kt | 57 ++++++++++------- .../web/EntityAccessControlHandlerTests.kt | 63 ++++++++++++++++++- .../egm/stellio/shared/model/NgsiLdEntity.kt | 4 +- 4 files changed, 99 insertions(+), 28 deletions(-) diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt index 6555b8b06..db389068b 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt @@ -10,11 +10,12 @@ interface AuthorizationService { const val USER_LABEL = AUTHORIZATION_ONTOLOGY + "User" const val GROUP_LABEL = AUTHORIZATION_ONTOLOGY + "Group" const val CLIENT_LABEL = AUTHORIZATION_ONTOLOGY + "Client" - val IAM_LABELS = listOf(USER_LABEL, GROUP_LABEL, CLIENT_LABEL) + val IAM_LABELS = setOf(USER_LABEL, GROUP_LABEL, CLIENT_LABEL) const val EGM_ROLES = AUTHORIZATION_ONTOLOGY + "roles" const val R_CAN_READ = AUTHORIZATION_ONTOLOGY + "rCanRead" const val R_CAN_WRITE = AUTHORIZATION_ONTOLOGY + "rCanWrite" const val R_CAN_ADMIN = AUTHORIZATION_ONTOLOGY + "rCanAdmin" + val IAM_RIGHTS = setOf(R_CAN_READ, R_CAN_WRITE, R_CAN_ADMIN) const val R_IS_MEMBER_OF = AUTHORIZATION_ONTOLOGY + "isMemberOf" const val SERVICE_ACCOUNT_ID = AUTHORIZATION_ONTOLOGY + "serviceAccountId" const val ADMIN_ROLE_LABEL = "stellio-admin" diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt index 18e90a644..28d1a55a4 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt @@ -1,11 +1,11 @@ package com.egm.stellio.entity.web import com.egm.stellio.entity.authorization.AuthorizationService +import com.egm.stellio.entity.model.NotUpdatedDetails import com.egm.stellio.entity.model.updateResultFromDetailedResult import com.egm.stellio.entity.service.EntityEventService import com.egm.stellio.entity.service.EntityService import com.egm.stellio.shared.model.NgsiLdRelationship -import com.egm.stellio.shared.model.NgsiLdRelationshipInstance import com.egm.stellio.shared.model.ResourceNotFoundException import com.egm.stellio.shared.model.parseToNgsiLdAttributes import com.egm.stellio.shared.util.JSON_LD_CONTENT_TYPE @@ -48,47 +48,56 @@ class EntityAccessControlHandler( val jsonLdAttributes = JsonLdUtils.expandJsonLdFragment(body, contexts) val ngsiLdAttributes = parseToNgsiLdAttributes(jsonLdAttributes) - val (authorizedInstances, unauthorizedInstances) = ngsiLdAttributes - .map { ngsiLdAttribute -> - (ngsiLdAttribute.getAttributeInstances() as List) - .map { Pair(ngsiLdAttribute, it) } - }.flatten() + // ensure payload contains only relationships and that they are of a known type + val (validAttributes, invalidAttributes) = ngsiLdAttributes.partition { + it is NgsiLdRelationship && + AuthorizationService.IAM_RIGHTS.contains(it.name) + } + val invalidAttributesDetails = invalidAttributes.map { + NotUpdatedDetails(it.compactName, "Not a relationship or not an authorized relationship name") + } + + val (authorizedInstances, unauthorizedInstances) = validAttributes + .map { it as NgsiLdRelationship } + .map { ngsiLdAttribute -> ngsiLdAttribute.getAttributeInstances().map { Pair(ngsiLdAttribute, it) } } + .flatten() .partition { // we don't have any sub-relationships here, so let's just take the first val targetEntityId = it.second.getLinkedEntitiesIds().first() authorizationService.userIsAdminOfEntity(targetEntityId, userId) } + val unauthorizedInstancesDetails = unauthorizedInstances.map { + NotUpdatedDetails( + it.first.compactName, + "User is not authorized to update rights on entity ${it.second.objectId}" + ) + } val results = authorizedInstances.map { - val ngsiLdRelationship = it.first as NgsiLdRelationship entityService.appendEntityRelationship( subjectId.toUri(), - ngsiLdRelationship, + it.first, it.second, false ) } + val appendResult = updateResultFromDetailedResult(results) - entityEventService.publishAttributeAppendEvents( - subjectId.toUri(), - jsonLdAttributes, - updateResultFromDetailedResult(results), - contexts - ) + if (appendResult.updated.isNotEmpty()) + entityEventService.publishAttributeAppendEvents( + subjectId.toUri(), + jsonLdAttributes, + appendResult, + contexts + ) - return if (unauthorizedInstances.isEmpty()) + return if (invalidAttributes.isEmpty() && unauthorizedInstances.isEmpty()) ResponseEntity.status(HttpStatus.NO_CONTENT).build() else { - val unauthorizedEntities = - unauthorizedInstances.map { it.second.objectId } - .joinToString(",") { "\"$it\"" } - ResponseEntity.status(HttpStatus.MULTI_STATUS).body( - """ - { - "unauthorized entities": [$unauthorizedEntities] - } - """.trimIndent() + val fullAppendResult = appendResult.copy( + notUpdated = appendResult.notUpdated.plus(invalidAttributesDetails).plus(unauthorizedInstancesDetails) ) + ResponseEntity.status(HttpStatus.MULTI_STATUS).body(fullAppendResult) } } diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt index 86652ffaf..93811d116 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt @@ -15,6 +15,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_EGM_AUTHORIZATION_CONTEXT import com.egm.stellio.shared.util.buildContextLinkHeader import com.egm.stellio.shared.util.toUri import com.ninjasquad.springmockk.MockkBean +import io.mockk.Called import io.mockk.Runs import io.mockk.every import io.mockk.just @@ -201,6 +202,13 @@ class EntityAccessControlHandlerTests { every { authorizationService.userIsAdminOfEntity(eq(entityUri1), any()) } returns true every { authorizationService.userIsAdminOfEntity(eq(entityUri2), any()) } returns false + every { + entityService.appendEntityRelationship(any(), any(), any(), any()) + } returns UpdateAttributeResult( + attributeName = "rCanRead", + datasetId = entityUri1, + updateOperationResult = UpdateOperationResult.APPENDED + ) webClient.post() .uri("/ngsi-ld/v1/entityAccessControl/$subjectId/attrs") @@ -209,7 +217,15 @@ class EntityAccessControlHandlerTests { .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) .expectBody().json( """ - { "unauthorized entities": ["urn:ngsi-ld:Entity:entityId2"]} + { + "updated":["rCanRead"], + "notUpdated":[ + { + "attributeName":"rCanRead", + "reason":"User is not authorized to update rights on entity urn:ngsi-ld:Entity:entityId2" + } + ] + } """ ) @@ -223,6 +239,51 @@ class EntityAccessControlHandlerTests { } } + @Test + fun `it should filter out invalid attributes when adding rights on entities`() { + val entityUri2 = "urn:ngsi-ld:Entity:entityId2".toUri() + val requestPayload = + """ + { + "invalidRelationshipName": [{ + "type": "Relationship", + "object": "$entityUri1", + "datasetId": "$entityUri1" + }], + "aProperty": { + "type": "Property", + "value": "$entityUri2" + }, + "@context": ["$NGSILD_EGM_AUTHORIZATION_CONTEXT", "$NGSILD_CORE_CONTEXT"] + } + """.trimIndent() + + webClient.post() + .uri("/ngsi-ld/v1/entityAccessControl/$subjectId/attrs") + .bodyValue(requestPayload) + .exchange() + .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) + .expectBody().json( + """ + { + "updated":[], + "notUpdated":[ + { + "attributeName":"invalidRelationshipName", + "reason":"Not a relationship or not an authorized relationship name" + }, + { + "attributeName":"aProperty", + "reason":"Not a relationship or not an authorized relationship name" + } + ] + } + """ + ) + + verify { entityService.appendEntityRelationship(any(), any(), any(), any()) wasNot Called } + } + @Test fun `it should allow an authorized user to remove access to an entity`() { every { authorizationService.userIsAdminOfEntity(any(), any()) } returns true diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/model/NgsiLdEntity.kt b/shared/src/main/kotlin/com/egm/stellio/shared/model/NgsiLdEntity.kt index 217b5ec15..181222d84 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/model/NgsiLdEntity.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/model/NgsiLdEntity.kt @@ -114,7 +114,7 @@ class NgsiLdProperty private constructor( override fun getLinkedEntitiesIds(): List = instances.flatMap { it.getLinkedEntitiesIds() } - override fun getAttributeInstances(): List = instances + override fun getAttributeInstances(): List = instances } class NgsiLdRelationship private constructor( @@ -139,7 +139,7 @@ class NgsiLdRelationship private constructor( override fun getLinkedEntitiesIds(): List = instances.flatMap { it.getLinkedEntitiesIds() } - override fun getAttributeInstances(): List = instances + override fun getAttributeInstances(): List = instances } class NgsiLdGeoProperty private constructor( From 0ea1293c32c5a49c8c9a85f034ef4254c0718eb6 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Sat, 4 Dec 2021 17:32:15 +0100 Subject: [PATCH 14/28] feat(entity): review and improve endpoint to remove rights of an user on an entity --- .../Neo4jAuthorizationRepository.kt | 4 +-- .../Neo4jAuthorizationService.kt | 8 +++--- .../entity/web/EntityAccessControlHandler.kt | 25 ++++++++++--------- .../web/EntityAccessControlHandlerTests.kt | 2 +- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt index 289d47a06..86b844faa 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt @@ -153,13 +153,13 @@ class Neo4jAuthorizationRepository( ): Int { val matchQuery = """ - MATCH (subject:Entity { id: ${'$'}entityId })-[:HAS_OBJECT]-(relNode) + MATCH (subject:Entity { id: ${'$'}subjectId })-[:HAS_OBJECT]-(relNode) -[]->(target:Entity { id: ${'$'}targetId }) DETACH DELETE relNode """.trimIndent() val parameters = mapOf( - "entityId" to subjectId.toString(), + "subjectId" to subjectId.toString(), "targetId" to targetId.toString() ) diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt index a5964bf4b..1753d6fa8 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt @@ -35,9 +35,9 @@ class Neo4jAuthorizationService( entitiesId, listOf(SpecificAccessPolicy.AUTH_WRITE, SpecificAccessPolicy.AUTH_READ) ) - // remove the already authorized entities from the list to avoid double checking them + // remove the already authorized entities from the list to avoid double-checking them val grantedEntities = filterEntitiesUserHaveOneOfGivenRights( - entitiesId.minus(authorizedBySpecificPolicyEntities), + entitiesId.minus(authorizedBySpecificPolicyEntities.toSet()), READ_RIGHT, userSub ) @@ -47,9 +47,9 @@ class Neo4jAuthorizationService( override fun filterEntitiesUserCanUpdate(entitiesId: List, userSub: String): List { val authorizedBySpecificPolicyEntities = filterEntitiesWithSpecificAccessPolicy(entitiesId, listOf(SpecificAccessPolicy.AUTH_WRITE)) - // remove the already authorized entities from the list to avoid double checking them + // remove the already authorized entities from the list to avoid double-checking them val grantedEntities = filterEntitiesUserHaveOneOfGivenRights( - entitiesId.minus(authorizedBySpecificPolicyEntities), + entitiesId.minus(authorizedBySpecificPolicyEntities.toSet()), WRITE_RIGHT, userSub ) diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt index 28d1a55a4..823b8f173 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt @@ -69,7 +69,7 @@ class EntityAccessControlHandler( val unauthorizedInstancesDetails = unauthorizedInstances.map { NotUpdatedDetails( it.first.compactName, - "User is not authorized to update rights on entity ${it.second.objectId}" + "User is not authorized to manage rights on entity ${it.second.objectId}" ) } @@ -114,18 +114,19 @@ class EntityAccessControlHandler( return ResponseEntity.status(HttpStatus.FORBIDDEN) .body("User is not authorized to manage rights on entity $entityId") - val removeResult = authorizationService.removeUserRightsOnEntity(entityId.toUri(), subjectId.toUri()) + val removeResult = + authorizationService.removeUserRightsOnEntity(entityId.toUri(), subjectId.toUri()) + .also { + if (it != 0) + entityEventService.publishAttributeDeleteEvent( + entityId = subjectId.toUri(), + attributeName = entityId, + deleteAll = false, + contexts = contexts + ) + } - if (removeResult == 1) { - entityEventService.publishAttributeDeleteEvent( - entityId = subjectId.toUri(), - attributeName = entityId, - deleteAll = false, - contexts = contexts - ) - } - - return if (removeResult == 1) + return if (removeResult != 0) ResponseEntity.status(HttpStatus.NO_CONTENT).build() else throw ResourceNotFoundException("Subject $subjectId has no right on entity $entityId") diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt index 93811d116..6b3775fe5 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt @@ -222,7 +222,7 @@ class EntityAccessControlHandlerTests { "notUpdated":[ { "attributeName":"rCanRead", - "reason":"User is not authorized to update rights on entity urn:ngsi-ld:Entity:entityId2" + "reason":"User is not authorized to manage rights on entity urn:ngsi-ld:Entity:entityId2" } ] } From d06297bb0ad120bf0e378aa449854d2f03ca6498 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Sun, 5 Dec 2021 16:49:02 +0100 Subject: [PATCH 15/28] refactor: move some authz related constants to the shared authz file --- .../authorization/AuthorizationService.kt | 4 --- .../Neo4jAuthorizationService.kt | 4 +-- .../entity/util/ApiTestsBootstrapper.kt | 2 +- .../entity/web/EntityAccessControlHandler.kt | 2 +- .../egm/stellio/entity/web/EntityHandler.kt | 2 +- .../entity/web/EntityOperationHandler.kt | 2 +- .../Neo4jAuthorizationServiceTest.kt | 2 +- .../entity/service/IAMListenerTests.kt | 6 +++-- .../search/model/SubjectAccessRights.kt | 2 +- .../egm/stellio/search/service/IAMListener.kt | 9 ++++--- .../service/SubjectAccessRightsService.kt | 23 ++++++++++++++-- .../search/web/TemporalEntityHandler.kt | 8 +++++- .../search/service/IAMListenerTests.kt | 4 +-- .../SubjectAccessRightsServiceTests.kt | 19 ++++++------- .../search/web/TemporalEntityHandlerTests.kt | 27 ++++++++++++++++++- .../stellio/shared/{web => util}/AuthUtils.kt | 13 +++++---- .../subscription/web/SubscriptionHandler.kt | 2 +- 17 files changed, 90 insertions(+), 41 deletions(-) rename shared/src/main/kotlin/com/egm/stellio/shared/{web => util}/AuthUtils.kt (73%) diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt index db389068b..86e7c0db8 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt @@ -18,10 +18,6 @@ interface AuthorizationService { val IAM_RIGHTS = setOf(R_CAN_READ, R_CAN_WRITE, R_CAN_ADMIN) const val R_IS_MEMBER_OF = AUTHORIZATION_ONTOLOGY + "isMemberOf" const val SERVICE_ACCOUNT_ID = AUTHORIZATION_ONTOLOGY + "serviceAccountId" - const val ADMIN_ROLE_LABEL = "stellio-admin" - const val CREATION_ROLE_LABEL = "stellio-creator" - val ADMIN_ROLES: Set = setOf(ADMIN_ROLE_LABEL) - val CREATION_ROLES: Set = setOf(CREATION_ROLE_LABEL).plus(ADMIN_ROLES) val ADMIN_RIGHT: Set = setOf(R_CAN_ADMIN) val WRITE_RIGHT: Set = setOf(R_CAN_WRITE).plus(ADMIN_RIGHT) val READ_RIGHT: Set = setOf(R_CAN_READ).plus(WRITE_RIGHT) diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt index 1753d6fa8..ee3e874c0 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt @@ -1,14 +1,14 @@ package com.egm.stellio.entity.authorization import com.egm.stellio.entity.authorization.AuthorizationService.Companion.ADMIN_RIGHT -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.ADMIN_ROLES -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.CREATION_ROLES import com.egm.stellio.entity.authorization.AuthorizationService.Companion.READ_RIGHT import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_ADMIN import com.egm.stellio.entity.authorization.AuthorizationService.Companion.USER_PREFIX import com.egm.stellio.entity.authorization.AuthorizationService.Companion.WRITE_RIGHT import com.egm.stellio.entity.authorization.AuthorizationService.SpecificAccessPolicy import com.egm.stellio.entity.model.Relationship +import com.egm.stellio.shared.util.ADMIN_ROLES +import com.egm.stellio.shared.util.CREATION_ROLES import com.egm.stellio.shared.util.toUri import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.stereotype.Component diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt index 1b1cb2bc0..a7d442b1b 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt @@ -1,11 +1,11 @@ package com.egm.stellio.entity.util import com.egm.stellio.entity.authorization.AuthorizationService.Companion.AUTHORIZATION_ONTOLOGY -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.CREATION_ROLE_LABEL import com.egm.stellio.entity.authorization.AuthorizationService.Companion.USER_PREFIX import com.egm.stellio.entity.model.Entity import com.egm.stellio.entity.model.Property import com.egm.stellio.entity.repository.EntityRepository +import com.egm.stellio.shared.util.CREATION_ROLE_LABEL import com.egm.stellio.shared.util.JsonLdUtils.EGM_BASE_CONTEXT_URL import com.egm.stellio.shared.util.toUri import org.springframework.beans.factory.annotation.Value diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt index 823b8f173..e30b518d1 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt @@ -11,9 +11,9 @@ import com.egm.stellio.shared.model.parseToNgsiLdAttributes import com.egm.stellio.shared.util.JSON_LD_CONTENT_TYPE import com.egm.stellio.shared.util.JsonLdUtils import com.egm.stellio.shared.util.checkAndGetContext +import com.egm.stellio.shared.util.extractSubjectOrEmpty import com.egm.stellio.shared.util.getContextFromLinkHeaderOrDefault import com.egm.stellio.shared.util.toUri -import com.egm.stellio.shared.web.extractSubjectOrEmpty import kotlinx.coroutines.reactive.awaitFirst import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityHandler.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityHandler.kt index db1b4d5b2..ec42c145a 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityHandler.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityHandler.kt @@ -16,7 +16,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.parseAndExpandAttributeFragment import com.egm.stellio.shared.util.JsonLdUtils.reconstructPolygonCoordinates import com.egm.stellio.shared.util.JsonLdUtils.removeContextFromInput import com.egm.stellio.shared.util.JsonUtils.serializeObject -import com.egm.stellio.shared.web.extractSubjectOrEmpty +import com.egm.stellio.shared.util.extractSubjectOrEmpty import kotlinx.coroutines.reactive.awaitFirst import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityOperationHandler.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityOperationHandler.kt index 4454898d6..80977d3bb 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityOperationHandler.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityOperationHandler.kt @@ -10,7 +10,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdEntities import com.egm.stellio.shared.util.JsonLdUtils.extractContextFromInput import com.egm.stellio.shared.util.JsonLdUtils.removeContextFromInput import com.egm.stellio.shared.util.JsonUtils.serializeObject -import com.egm.stellio.shared.web.extractSubjectOrEmpty +import com.egm.stellio.shared.util.extractSubjectOrEmpty import kotlinx.coroutines.reactive.awaitFirst import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationServiceTest.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationServiceTest.kt index 5615c2051..0cc23d015 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationServiceTest.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationServiceTest.kt @@ -1,9 +1,9 @@ package com.egm.stellio.entity.authorization import com.egm.stellio.entity.authorization.AuthorizationService.* -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.ADMIN_ROLE_LABEL import com.egm.stellio.entity.authorization.AuthorizationService.Companion.READ_RIGHT import com.egm.stellio.entity.authorization.AuthorizationService.Companion.WRITE_RIGHT +import com.egm.stellio.shared.util.ADMIN_ROLE_LABEL import com.egm.stellio.shared.util.toListOfUri import com.egm.stellio.shared.util.toUri import com.ninjasquad.springmockk.MockkBean diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/service/IAMListenerTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/service/IAMListenerTests.kt index 7c45116e3..2a9312e8d 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/service/IAMListenerTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/service/IAMListenerTests.kt @@ -2,6 +2,8 @@ package com.egm.stellio.entity.service import com.egm.stellio.shared.model.NgsiLdProperty import com.egm.stellio.shared.model.NgsiLdRelationship +import com.egm.stellio.shared.util.ADMIN_ROLE_LABEL +import com.egm.stellio.shared.util.CREATION_ROLE_LABEL import com.egm.stellio.shared.util.loadSampleData import com.egm.stellio.shared.util.toUri import com.ninjasquad.springmockk.MockkBean @@ -184,7 +186,7 @@ class IAMListenerTests { (it[0] as NgsiLdProperty).instances.size == 1 && (it[0] as NgsiLdProperty).instances[0].value is List<*> && ((it[0] as NgsiLdProperty).instances[0].value as List<*>) - .containsAll(setOf("stellio-admin", "stellio-creator")) + .containsAll(setOf(ADMIN_ROLE_LABEL, CREATION_ROLE_LABEL)) }, false ) @@ -207,7 +209,7 @@ class IAMListenerTests { it[0] is NgsiLdProperty && (it[0] as NgsiLdProperty).instances.size == 1 && (it[0] as NgsiLdProperty).instances[0].value is String && - (it[0] as NgsiLdProperty).instances[0].value == "stellio-admin" + (it[0] as NgsiLdProperty).instances[0].value == ADMIN_ROLE_LABEL }, false ) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectAccessRights.kt b/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectAccessRights.kt index 43b5d9876..7b96e6b56 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectAccessRights.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectAccessRights.kt @@ -1,6 +1,6 @@ package com.egm.stellio.search.model -import com.egm.stellio.shared.web.SubjectType +import com.egm.stellio.shared.util.SubjectType import org.springframework.data.annotation.Id import java.util.UUID diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt index 9a075b1c1..4dfe2ce18 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt @@ -7,10 +7,11 @@ import com.egm.stellio.shared.model.AttributeReplaceEvent import com.egm.stellio.shared.model.EntityCreateEvent import com.egm.stellio.shared.model.EntityDeleteEvent import com.egm.stellio.shared.model.EntityEvent +import com.egm.stellio.shared.util.ADMIN_ROLE_LABEL import com.egm.stellio.shared.util.JsonUtils +import com.egm.stellio.shared.util.SubjectType +import com.egm.stellio.shared.util.extractSubjectUuid import com.egm.stellio.shared.util.toUri -import com.egm.stellio.shared.web.extractSubjectUuid -import com.egm.stellio.shared.web.getSubjectTypeFromSubjectId import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.slf4j.LoggerFactory @@ -48,7 +49,7 @@ class IAMListener( private fun createSubjectAccessRights(entityCreateEvent: EntityCreateEvent) { val userAccessRights = SubjectAccessRights( subjectId = entityCreateEvent.entityId.extractSubjectUuid(), - subjectType = getSubjectTypeFromSubjectId(entityCreateEvent.entityId) + subjectType = SubjectType.valueOf(entityCreateEvent.entityType.uppercase()) ) subjectAccessRightsService.create(userAccessRights) @@ -70,7 +71,7 @@ class IAMListener( val updatedRoles = (operationPayloadNode["value"] as ArrayNode).elements() var hasStellioAdminRole = false while (updatedRoles.hasNext()) { - if (updatedRoles.next().asText().equals("stellio-admin")) { + if (updatedRoles.next().asText().equals(ADMIN_ROLE_LABEL)) { hasStellioAdminRole = true break } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt index 0afd87467..2f430995e 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt @@ -1,7 +1,8 @@ package com.egm.stellio.search.service import com.egm.stellio.search.model.SubjectAccessRights -import com.egm.stellio.shared.web.SubjectType +import com.egm.stellio.shared.util.ADMIN_ROLE_LABEL +import com.egm.stellio.shared.util.SubjectType import org.slf4j.LoggerFactory import org.springframework.data.r2dbc.core.R2dbcEntityTemplate import org.springframework.data.relational.core.query.Criteria @@ -111,7 +112,7 @@ class SubjectAccessRightsService( databaseClient.sql( """ UPDATE subject_access_rights - SET global_role = 'stellio-admin' + SET global_role = '$ADMIN_ROLE_LABEL' WHERE subject_id = :subject_id """ ) @@ -154,6 +155,24 @@ class SubjectAccessRightsService( } .onErrorReturn(false) + fun hasWriteRoleOnEntity(subjectId: UUID, entityId: URI): Mono = + databaseClient.sql( + """ + SELECT COUNT(subject_id) as count + FROM subject_access_rights + WHERE subject_id = :subject_id + AND (:entity_id = ANY(allowed_write_entities)) + """ + ) + .bind("subject_id", subjectId) + .bind("entity_id", entityId) + .fetch() + .one() + .map { + it["count"] as Long == 1L + } + .onErrorReturn(false) + @Transactional fun delete(subjectId: UUID): Mono = r2dbcEntityTemplate.delete(SubjectAccessRights::class.java) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt index 6a92a99a7..f73e6bfe5 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt @@ -20,7 +20,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.compactTerm import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdFragment import com.egm.stellio.shared.util.JsonLdUtils.expandValueAsListOfMap import com.egm.stellio.shared.util.JsonUtils.serializeObject -import com.egm.stellio.shared.web.extractSubjectOrEmpty +import com.egm.stellio.shared.util.extractSubjectOrEmpty import kotlinx.coroutines.reactive.awaitFirst import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus @@ -58,6 +58,12 @@ class TemporalEntityHandler( @PathVariable entityId: String, @RequestBody requestBody: Mono ): ResponseEntity<*> { + val userId = extractSubjectOrEmpty().awaitFirst() + val canWriteEntity = + subjectAccessRightsService.hasWriteRoleOnEntity(UUID.fromString(userId), entityId.toUri()).awaitFirst() + if (!canWriteEntity) + throw AccessDeniedException("User forbidden write access to entity $entityId") + val body = requestBody.awaitFirst() val contexts = checkAndGetContext(httpHeaders, body) val jsonLdAttributes = expandJsonLdFragment(body, contexts) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt index 4423bbc28..ae3a805b0 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt @@ -1,9 +1,9 @@ package com.egm.stellio.search.service +import com.egm.stellio.shared.util.SubjectType import com.egm.stellio.shared.util.loadSampleData +import com.egm.stellio.shared.util.toUUID import com.egm.stellio.shared.util.toUri -import com.egm.stellio.shared.web.SubjectType -import com.egm.stellio.shared.web.toUUID import com.ninjasquad.springmockk.MockkBean import io.mockk.confirmVerified import io.mockk.verify diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt index 4e3588839..3effac620 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt @@ -2,8 +2,9 @@ package com.egm.stellio.search.service import com.egm.stellio.search.model.SubjectAccessRights import com.egm.stellio.search.support.WithTimescaleContainer +import com.egm.stellio.shared.util.ADMIN_ROLE_LABEL +import com.egm.stellio.shared.util.SubjectType import com.egm.stellio.shared.util.toUri -import com.egm.stellio.shared.web.SubjectType import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired @@ -37,7 +38,7 @@ class SubjectAccessRightsServiceTests : WithTimescaleContainer { val userAccessRights = SubjectAccessRights( subjectId = subjectUuid, subjectType = SubjectType.USER, - globalRole = "stellio-admin", + globalRole = ADMIN_ROLE_LABEL, allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678"), allowedWriteEntities = arrayOf("urn:ngsi-ld:Entity:6666", "urn:ngsi-ld:Entity:0000") ) @@ -57,7 +58,7 @@ class SubjectAccessRightsServiceTests : WithTimescaleContainer { val userAccessRights = SubjectAccessRights( subjectId = subjectUuid, subjectType = SubjectType.USER, - globalRole = "stellio-admin", + globalRole = ADMIN_ROLE_LABEL, allowedReadEntities = allowedReadEntities, allowedWriteEntities = allowedWriteEntities ) @@ -70,7 +71,7 @@ class SubjectAccessRightsServiceTests : WithTimescaleContainer { .expectNextMatches { it.subjectId == subjectUuid && it.subjectType == SubjectType.USER && - it.globalRole == "stellio-admin" && + it.globalRole == ADMIN_ROLE_LABEL && it.allowedReadEntities?.contentEquals(allowedReadEntities) == true && it.allowedWriteEntities?.contentEquals(allowedWriteEntities) == true } @@ -83,7 +84,7 @@ class SubjectAccessRightsServiceTests : WithTimescaleContainer { val userAccessRights = SubjectAccessRights( subjectId = subjectUuid, subjectType = SubjectType.USER, - globalRole = "stellio-admin", + globalRole = ADMIN_ROLE_LABEL, allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678") ) @@ -109,7 +110,7 @@ class SubjectAccessRightsServiceTests : WithTimescaleContainer { val userAccessRights = SubjectAccessRights( subjectId = subjectUuid, subjectType = SubjectType.USER, - globalRole = "stellio-admin", + globalRole = ADMIN_ROLE_LABEL, allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678") ) @@ -152,7 +153,7 @@ class SubjectAccessRightsServiceTests : WithTimescaleContainer { subjectAccessRightsService.retrieve(subjectUuid) ) .expectNextMatches { - it.globalRole == "stellio-admin" + it.globalRole == ADMIN_ROLE_LABEL } .expectComplete() .verify() @@ -179,7 +180,7 @@ class SubjectAccessRightsServiceTests : WithTimescaleContainer { val userAccessRights = SubjectAccessRights( subjectId = subjectUuid, subjectType = SubjectType.USER, - globalRole = "stellio-admin", + globalRole = ADMIN_ROLE_LABEL, allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678"), allowedWriteEntities = arrayOf("urn:ngsi-ld:Entity:6666") ) @@ -213,7 +214,7 @@ class SubjectAccessRightsServiceTests : WithTimescaleContainer { val userAccessRights = SubjectAccessRights( subjectId = subjectUuid, subjectType = SubjectType.USER, - globalRole = "stellio-admin", + globalRole = ADMIN_ROLE_LABEL, allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678"), allowedWriteEntities = arrayOf("urn:ngsi-ld:Entity:6666", "urn:ngsi-ld:Entity:0000") ) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt index 5faa00027..e21fd0023 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt @@ -21,6 +21,7 @@ import com.egm.stellio.shared.util.entityNotFoundMessage import com.egm.stellio.shared.util.loadSampleData import com.egm.stellio.shared.util.toUri import com.ninjasquad.springmockk.MockkBean +import io.mockk.Called import io.mockk.coEvery import io.mockk.coVerify import io.mockk.confirmVerified @@ -91,6 +92,7 @@ class TemporalEntityHandlerTests { loadSampleData("fragments/temporal_entity_fragment_one_attribute_one_instance.jsonld") val temporalEntityAttributeUuid = UUID.randomUUID() + every { subjectAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } every { temporalEntityAttributeService.getForEntityAndAttribute(any(), any()) } answers { Mono.just(temporalEntityAttributeUuid) } @@ -132,6 +134,7 @@ class TemporalEntityHandlerTests { loadSampleData("fragments/temporal_entity_fragment_one_attribute_many_instances.jsonld") val temporalEntityAttributeUuid = UUID.randomUUID() + every { subjectAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } every { temporalEntityAttributeService.getForEntityAndAttribute(any(), any()) } answers { Mono.just(temporalEntityAttributeUuid) } @@ -173,6 +176,7 @@ class TemporalEntityHandlerTests { loadSampleData("fragments/temporal_entity_fragment_many_attributes_one_instance.jsonld") val temporalEntityAttributeUuid = UUID.randomUUID() + every { subjectAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } every { temporalEntityAttributeService.getForEntityAndAttribute(any(), any()) } answers { Mono.just(temporalEntityAttributeUuid) } @@ -217,6 +221,7 @@ class TemporalEntityHandlerTests { loadSampleData("fragments/temporal_entity_fragment_many_attributes_many_instances.jsonld") val temporalEntityAttributeUuid = UUID.randomUUID() + every { subjectAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } every { temporalEntityAttributeService.getForEntityAndAttribute(any(), any()) } answers { Mono.just(temporalEntityAttributeUuid) } @@ -257,8 +262,10 @@ class TemporalEntityHandlerTests { @Test fun `it should return a 400 if temporal entity fragment is badly formed`() { + every { subjectAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } + webClient.post() - .uri("/ngsi-ld/v1/temporal/entities/entityId/attrs") + .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs") .header("Link", buildContextLinkHeader(APIC_COMPOUND_CONTEXT)) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue("{ \"id\": \"bad\" }")) @@ -275,6 +282,24 @@ class TemporalEntityHandlerTests { ) } + @Test + fun `it should return a 403 is user is not authorized to write on the entity`() { + every { subjectAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(false) } + + webClient.post() + .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs") + .header("Link", buildContextLinkHeader(APIC_COMPOUND_CONTEXT)) + .contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue("")) + .exchange() + .expectStatus().isForbidden + + verify { temporalEntityAttributeService.getForEntityAndAttribute(any(), any()) wasNot Called } + verify { attributeInstanceService.addAttributeInstance(any(), any(), any(), any()) wasNot Called } + confirmVerified(temporalEntityAttributeService) + confirmVerified(attributeInstanceService) + } + @Test fun `it should raise a 400 if timerel is present without time query param`() { every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/web/AuthUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt similarity index 73% rename from shared/src/main/kotlin/com/egm/stellio/shared/web/AuthUtils.kt rename to shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt index d6c3117b2..e6bbb9c17 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/web/AuthUtils.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt @@ -1,13 +1,17 @@ -package com.egm.stellio.shared.web +package com.egm.stellio.shared.util import org.springframework.security.core.context.ReactiveSecurityContextHolder import org.springframework.security.core.context.SecurityContextImpl import org.springframework.security.oauth2.jwt.Jwt import reactor.core.publisher.Mono import java.net.URI -import java.util.Locale import java.util.UUID +const val ADMIN_ROLE_LABEL = "stellio-admin" +const val CREATION_ROLE_LABEL = "stellio-creator" +val ADMIN_ROLES: Set = setOf(ADMIN_ROLE_LABEL) +val CREATION_ROLES: Set = setOf(CREATION_ROLE_LABEL).plus(ADMIN_ROLES) + fun extractSubjectOrEmpty(): Mono { return ReactiveSecurityContextHolder.getContext() .switchIfEmpty(Mono.just(SecurityContextImpl())) @@ -20,11 +24,6 @@ fun URI.extractSubjectUuid(): UUID = fun String.toUUID(): UUID = UUID.fromString(this) -fun getSubjectTypeFromSubjectId(subjectId: URI): SubjectType { - val type = subjectId.toString().split(":")[2] - return SubjectType.valueOf(type.uppercase(Locale.getDefault())) -} - enum class SubjectType { USER, GROUP, diff --git a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt index f0ec2b2b4..f8880655b 100644 --- a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt +++ b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt @@ -13,10 +13,10 @@ import com.egm.stellio.shared.util.QUERY_PARAM_COUNT import com.egm.stellio.shared.util.buildGetSuccessResponse import com.egm.stellio.shared.util.checkAndGetContext import com.egm.stellio.shared.util.extractAndValidatePaginationParameters +import com.egm.stellio.shared.util.extractSubjectOrEmpty import com.egm.stellio.shared.util.getApplicableMediaType import com.egm.stellio.shared.util.getContextFromLinkHeaderOrDefault import com.egm.stellio.shared.util.toUri -import com.egm.stellio.shared.web.extractSubjectOrEmpty import com.egm.stellio.subscription.config.ApplicationProperties import com.egm.stellio.subscription.model.Subscription import com.egm.stellio.subscription.model.toJson From 58645e41f7fad19c8f89f2b1212834587c0c80ad Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Thu, 9 Dec 2021 09:44:34 +0100 Subject: [PATCH 16/28] feat(search): refactor authz model to handle groups and improve later performance --- .../Neo4jAuthorizationService.kt | 4 +- .../entity/util/ApiTestsBootstrapper.kt | 4 +- .../Neo4jAuthorizationServiceTest.kt | 6 +- .../entity/service/IAMListenerTests.kt | 7 +- .../search/model/EntityAccessRights.kt | 11 + ...tAccessRights.kt => SubjectReferential.kt} | 10 +- .../service/EntityAccessRightsService.kt | 108 ++++++++ .../egm/stellio/search/service/IAMListener.kt | 60 ++--- .../service/SubjectAccessRightsService.kt | 190 -------------- .../service/SubjectReferentialService.kt | 113 +++++++++ .../search/web/TemporalEntityHandler.kt | 8 +- .../V0_17__add_user_access_rights_table.sql | 18 +- .../service/EntityAccessRightsServiceTests.kt | 103 ++++++++ .../search/service/IAMListenerTests.kt | 41 +-- .../SubjectAccessRightsServiceTests.kt | 237 ------------------ .../service/SubjectReferentialServiceTests.kt | 130 ++++++++++ .../search/web/TemporalEntityHandlerTests.kt | 44 ++-- .../com/egm/stellio/shared/util/AuthUtils.kt | 31 ++- 18 files changed, 595 insertions(+), 530 deletions(-) create mode 100644 search-service/src/main/kotlin/com/egm/stellio/search/model/EntityAccessRights.kt rename search-service/src/main/kotlin/com/egm/stellio/search/model/{SubjectAccessRights.kt => SubjectReferential.kt} (71%) create mode 100644 search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt delete mode 100644 search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt create mode 100644 search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt create mode 100644 search-service/src/test/kotlin/com/egm/stellio/search/service/EntityAccessRightsServiceTests.kt delete mode 100644 search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt create mode 100644 search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectReferentialServiceTests.kt diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt index ee3e874c0..dc2571db5 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt @@ -9,6 +9,7 @@ import com.egm.stellio.entity.authorization.AuthorizationService.SpecificAccessP import com.egm.stellio.entity.model.Relationship import com.egm.stellio.shared.util.ADMIN_ROLES import com.egm.stellio.shared.util.CREATION_ROLES +import com.egm.stellio.shared.util.GlobalRole import com.egm.stellio.shared.util.toUri import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.stereotype.Component @@ -24,8 +25,9 @@ class Neo4jAuthorizationService( override fun userCanCreateEntities(userSub: String): Boolean = userIsOneOfGivenRoles(CREATION_ROLES, userSub) - private fun userIsOneOfGivenRoles(roles: Set, userSub: String): Boolean = + private fun userIsOneOfGivenRoles(roles: Set, userSub: String): Boolean = neo4jAuthorizationRepository.getUserRoles((USER_PREFIX + userSub).toUri()) + .map { GlobalRole.forKey(it) } .intersect(roles) .isNotEmpty() diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt index a7d442b1b..deb7f8bf6 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt @@ -5,7 +5,7 @@ import com.egm.stellio.entity.authorization.AuthorizationService.Companion.USER_ import com.egm.stellio.entity.model.Entity import com.egm.stellio.entity.model.Property import com.egm.stellio.entity.repository.EntityRepository -import com.egm.stellio.shared.util.CREATION_ROLE_LABEL +import com.egm.stellio.shared.util.GlobalRole import com.egm.stellio.shared.util.JsonLdUtils.EGM_BASE_CONTEXT_URL import com.egm.stellio.shared.util.toUri import org.springframework.beans.factory.annotation.Value @@ -28,7 +28,7 @@ class ApiTestsBootstrapper( "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" ) const val USER_TYPE = "User" - val USER_ROLES = listOf(CREATION_ROLE_LABEL) + val USER_ROLES = listOf(GlobalRole.STELLIO_CREATOR.key) } override fun run(vararg args: String?) { diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationServiceTest.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationServiceTest.kt index 0cc23d015..b5b1ef0c1 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationServiceTest.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationServiceTest.kt @@ -3,7 +3,7 @@ package com.egm.stellio.entity.authorization import com.egm.stellio.entity.authorization.AuthorizationService.* import com.egm.stellio.entity.authorization.AuthorizationService.Companion.READ_RIGHT import com.egm.stellio.entity.authorization.AuthorizationService.Companion.WRITE_RIGHT -import com.egm.stellio.shared.util.ADMIN_ROLE_LABEL +import com.egm.stellio.shared.util.GlobalRole import com.egm.stellio.shared.util.toListOfUri import com.egm.stellio.shared.util.toUri import com.ninjasquad.springmockk.MockkBean @@ -106,7 +106,7 @@ class Neo4jAuthorizationServiceTest { fun `it should find admin user has admin, read or write right entity`() { every { neo4jAuthorizationRepository.getUserRoles(mockUserUri) - } returns setOf(ADMIN_ROLE_LABEL) + } returns setOf(GlobalRole.STELLIO_ADMIN.key) assert(neo4jAuthorizationService.userIsAdminOfEntity(entityUri, mockUserSub)) assert(neo4jAuthorizationService.userCanReadEntity(entityUri, mockUserSub)) @@ -200,7 +200,7 @@ class Neo4jAuthorizationServiceTest { } returns emptyList() every { neo4jAuthorizationRepository.getUserRoles(mockUserUri) - } returns setOf(ADMIN_ROLE_LABEL) + } returns setOf(GlobalRole.STELLIO_ADMIN.key) assert( neo4jAuthorizationService.filterEntitiesUserCanRead(entitiesIds, mockUserSub) == entitiesIds diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/service/IAMListenerTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/service/IAMListenerTests.kt index 2a9312e8d..926f32043 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/service/IAMListenerTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/service/IAMListenerTests.kt @@ -2,8 +2,7 @@ package com.egm.stellio.entity.service import com.egm.stellio.shared.model.NgsiLdProperty import com.egm.stellio.shared.model.NgsiLdRelationship -import com.egm.stellio.shared.util.ADMIN_ROLE_LABEL -import com.egm.stellio.shared.util.CREATION_ROLE_LABEL +import com.egm.stellio.shared.util.GlobalRole import com.egm.stellio.shared.util.loadSampleData import com.egm.stellio.shared.util.toUri import com.ninjasquad.springmockk.MockkBean @@ -186,7 +185,7 @@ class IAMListenerTests { (it[0] as NgsiLdProperty).instances.size == 1 && (it[0] as NgsiLdProperty).instances[0].value is List<*> && ((it[0] as NgsiLdProperty).instances[0].value as List<*>) - .containsAll(setOf(ADMIN_ROLE_LABEL, CREATION_ROLE_LABEL)) + .containsAll(setOf(GlobalRole.STELLIO_ADMIN.key, GlobalRole.STELLIO_CREATOR.key)) }, false ) @@ -209,7 +208,7 @@ class IAMListenerTests { it[0] is NgsiLdProperty && (it[0] as NgsiLdProperty).instances.size == 1 && (it[0] as NgsiLdProperty).instances[0].value is String && - (it[0] as NgsiLdProperty).instances[0].value == ADMIN_ROLE_LABEL + (it[0] as NgsiLdProperty).instances[0].value == GlobalRole.STELLIO_ADMIN.key }, false ) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/EntityAccessRights.kt b/search-service/src/main/kotlin/com/egm/stellio/search/model/EntityAccessRights.kt new file mode 100644 index 000000000..cc5be0ff9 --- /dev/null +++ b/search-service/src/main/kotlin/com/egm/stellio/search/model/EntityAccessRights.kt @@ -0,0 +1,11 @@ +package com.egm.stellio.search.model + +import com.egm.stellio.shared.util.AccessRight +import java.net.URI +import java.util.UUID + +data class EntityAccessRights( + val subjectId: UUID, + val accessRight: AccessRight, + val entityId: URI +) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectAccessRights.kt b/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectReferential.kt similarity index 71% rename from search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectAccessRights.kt rename to search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectReferential.kt index 7b96e6b56..bcbd600f2 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectAccessRights.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectReferential.kt @@ -1,22 +1,22 @@ package com.egm.stellio.search.model +import com.egm.stellio.shared.util.GlobalRole import com.egm.stellio.shared.util.SubjectType import org.springframework.data.annotation.Id import java.util.UUID -data class SubjectAccessRights( +data class SubjectReferential( @Id val subjectId: UUID, val subjectType: SubjectType, - val globalRole: String? = null, - val allowedReadEntities: Array? = null, - val allowedWriteEntities: Array? = null + val globalRoles: List? = null, + val groupsMemberships: List? = null ) { override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false - other as SubjectAccessRights + other as SubjectReferential if (subjectId != other.subjectId) return false diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt new file mode 100644 index 000000000..0015ad5ed --- /dev/null +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt @@ -0,0 +1,108 @@ +package com.egm.stellio.search.service + +import com.egm.stellio.search.model.EntityAccessRights +import com.egm.stellio.shared.util.AccessRight +import com.egm.stellio.shared.util.AccessRight.R_CAN_ADMIN +import com.egm.stellio.shared.util.AccessRight.R_CAN_READ +import com.egm.stellio.shared.util.AccessRight.R_CAN_WRITE +import org.slf4j.LoggerFactory +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate +import org.springframework.data.relational.core.query.Criteria +import org.springframework.data.relational.core.query.Query +import org.springframework.r2dbc.core.DatabaseClient +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import reactor.core.publisher.Mono +import java.net.URI +import java.util.UUID + +@Service +class EntityAccessRightsService( + private val databaseClient: DatabaseClient, + private val r2dbcEntityTemplate: R2dbcEntityTemplate, +) { + private val logger = LoggerFactory.getLogger(javaClass) + + @Transactional + fun setReadRoleOnEntity(subjectId: UUID, entityId: URI): Mono = + setRoleOnEntity(subjectId, entityId, R_CAN_READ) + + @Transactional + fun setWriteRoleOnEntity(subjectId: UUID, entityId: URI): Mono = + setRoleOnEntity(subjectId, entityId, R_CAN_WRITE) + + @Transactional + fun setRoleOnEntity(subjectId: UUID, entityId: URI, accessRight: AccessRight): Mono = + databaseClient + .sql( + """ + INSERT INTO entity_access_rights (subject_id, entity_id, access_right) + VALUES (:subject_id, :entity_id, :access_right) + ON CONFLICT (subject_id, entity_id, access_right) + DO UPDATE SET access_right = :access_right + """ + ) + .bind("subject_id", subjectId) + .bind("entity_id", entityId) + .bind("access_right", accessRight.toString()) + .fetch() + .rowsUpdated() + .thenReturn(1) + .onErrorResume { + logger.error("Error while setting access right on entity: $it") + Mono.just(-1) + } + + @Transactional + fun removeRoleOnEntity(subjectId: UUID, entityId: URI): Mono = + databaseClient + .sql( + """ + DELETE from entity_access_rights + WHERE subject_id = :subject_id + AND entity_id = :entity_id + """ + ) + .bind("subject_id", subjectId) + .bind("entity_id", entityId) + .fetch() + .rowsUpdated() + .thenReturn(1) + .onErrorReturn(-1) + + fun hasReadRoleOnEntity(subjectId: UUID, entityId: URI): Mono = + hasRoleOnEntity(subjectId, entityId, listOf(R_CAN_READ, R_CAN_WRITE, R_CAN_ADMIN)) + + fun hasWriteRoleOnEntity(subjectId: UUID, entityId: URI): Mono = + hasRoleOnEntity(subjectId, entityId, listOf(R_CAN_WRITE, R_CAN_ADMIN)) + + fun hasRoleOnEntity(subjectId: UUID, entityId: URI, accessRights: List): Mono = + databaseClient + .sql( + """ + SELECT COUNT(subject_id) as count + FROM entity_access_rights + WHERE subject_id = :subject_id + AND entity_id = :entity_id + AND access_right IN(:access_rights) + """ + ) + .bind("subject_id", subjectId) + .bind("entity_id", entityId) + .bind("access_rights", accessRights.map { it.toString() }) + .fetch() + .one() + .map { + it["count"] as Long == 1L + } + .onErrorResume { + logger.error("Error while checking role on entity: $it") + Mono.just(false) + } + + @Transactional + fun delete(subjectId: UUID): Mono = + r2dbcEntityTemplate.delete(EntityAccessRights::class.java) + .matching(Query.query(Criteria.where("subject_id").`is`(subjectId))) + .all() +} diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt index 4dfe2ce18..fc05f89f9 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt @@ -1,13 +1,14 @@ package com.egm.stellio.search.service -import com.egm.stellio.search.model.SubjectAccessRights +import com.egm.stellio.search.model.SubjectReferential import com.egm.stellio.shared.model.AttributeAppendEvent import com.egm.stellio.shared.model.AttributeDeleteEvent import com.egm.stellio.shared.model.AttributeReplaceEvent import com.egm.stellio.shared.model.EntityCreateEvent import com.egm.stellio.shared.model.EntityDeleteEvent import com.egm.stellio.shared.model.EntityEvent -import com.egm.stellio.shared.util.ADMIN_ROLE_LABEL +import com.egm.stellio.shared.util.AccessRight +import com.egm.stellio.shared.util.GlobalRole import com.egm.stellio.shared.util.JsonUtils import com.egm.stellio.shared.util.SubjectType import com.egm.stellio.shared.util.extractSubjectUuid @@ -20,7 +21,8 @@ import org.springframework.stereotype.Component @Component class IAMListener( - private val subjectAccessRightsService: SubjectAccessRightsService + private val subjectReferentialService: SubjectReferentialService, + private val entityAccessRightsService: EntityAccessRightsService ) { private val logger = LoggerFactory.getLogger(javaClass) @@ -28,8 +30,8 @@ class IAMListener( @KafkaListener(topics = ["cim.iam"], groupId = "search-iam") fun processMessage(content: String) { when (val authorizationEvent = JsonUtils.deserializeAs(content)) { - is EntityCreateEvent -> createSubjectAccessRights(authorizationEvent) - is EntityDeleteEvent -> deleteSubjectAccessRights(authorizationEvent) + is EntityCreateEvent -> createSubjectReferential(authorizationEvent) + is EntityDeleteEvent -> deleteSubjectReferential(authorizationEvent) is AttributeAppendEvent -> addRoleToSubject(authorizationEvent) is AttributeReplaceEvent -> TODO() is AttributeDeleteEvent -> TODO() @@ -46,20 +48,20 @@ class IAMListener( } } - private fun createSubjectAccessRights(entityCreateEvent: EntityCreateEvent) { - val userAccessRights = SubjectAccessRights( + private fun createSubjectReferential(entityCreateEvent: EntityCreateEvent) { + val subjectReferential = SubjectReferential( subjectId = entityCreateEvent.entityId.extractSubjectUuid(), subjectType = SubjectType.valueOf(entityCreateEvent.entityType.uppercase()) ) - subjectAccessRightsService.create(userAccessRights) + subjectReferentialService.create(subjectReferential) .subscribe { logger.debug("Created subject ${entityCreateEvent.entityId}") } } - private fun deleteSubjectAccessRights(entityDeleteEvent: EntityDeleteEvent) { - subjectAccessRightsService.delete(entityDeleteEvent.entityId.extractSubjectUuid()) + private fun deleteSubjectReferential(entityDeleteEvent: EntityDeleteEvent) { + subjectReferentialService.delete(entityDeleteEvent.entityId.extractSubjectUuid()) .subscribe { logger.debug("Deleted subject ${entityDeleteEvent.entityId}") } @@ -69,42 +71,28 @@ class IAMListener( if (attributeAppendEvent.attributeName == "roles") { val operationPayloadNode = jacksonObjectMapper().readTree(attributeAppendEvent.operationPayload) val updatedRoles = (operationPayloadNode["value"] as ArrayNode).elements() - var hasStellioAdminRole = false - while (updatedRoles.hasNext()) { - if (updatedRoles.next().asText().equals(ADMIN_ROLE_LABEL)) { - hasStellioAdminRole = true - break - } - } - if (hasStellioAdminRole) - subjectAccessRightsService.addAdminGlobalRole(attributeAppendEvent.entityId.extractSubjectUuid()) + val newRoles = updatedRoles.asSequence().map { + GlobalRole.forKey(it.asText()) + }.toList() + if (newRoles.isNotEmpty()) + subjectReferentialService.setGlobalRoles(attributeAppendEvent.entityId.extractSubjectUuid(), newRoles) else - subjectAccessRightsService.removeAdminGlobalRole(attributeAppendEvent.entityId.extractSubjectUuid()) + subjectReferentialService.resetGlobalRoles(attributeAppendEvent.entityId.extractSubjectUuid()) } } private fun addEntityToSubject(attributeAppendEvent: AttributeAppendEvent) { val operationPayloadNode = jacksonObjectMapper().readTree(attributeAppendEvent.operationPayload) val entityId = operationPayloadNode["object"].asText() - when (attributeAppendEvent.attributeName) { - "rCanRead" -> { - subjectAccessRightsService.addReadRoleOnEntity( - attributeAppendEvent.entityId.extractSubjectUuid(), - entityId.toUri() - ) - } - "rCanWrite" -> { - subjectAccessRightsService.addWriteRoleOnEntity( - attributeAppendEvent.entityId.extractSubjectUuid(), - entityId.toUri() - ) - } - else -> logger.warn("Unrecognized attribute name ${attributeAppendEvent.attributeName}") - } + entityAccessRightsService.setRoleOnEntity( + attributeAppendEvent.entityId.extractSubjectUuid(), + entityId.toUri(), + AccessRight.forAttributeName(attributeAppendEvent.attributeName) + ) } private fun removeEntityFromSubject(attributeDeleteEvent: AttributeDeleteEvent) { - subjectAccessRightsService.removeRoleOnEntity( + entityAccessRightsService.removeRoleOnEntity( attributeDeleteEvent.entityId.extractSubjectUuid(), attributeDeleteEvent.attributeName.toUri() ) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt deleted file mode 100644 index 2f430995e..000000000 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectAccessRightsService.kt +++ /dev/null @@ -1,190 +0,0 @@ -package com.egm.stellio.search.service - -import com.egm.stellio.search.model.SubjectAccessRights -import com.egm.stellio.shared.util.ADMIN_ROLE_LABEL -import com.egm.stellio.shared.util.SubjectType -import org.slf4j.LoggerFactory -import org.springframework.data.r2dbc.core.R2dbcEntityTemplate -import org.springframework.data.relational.core.query.Criteria -import org.springframework.data.relational.core.query.Query -import org.springframework.r2dbc.core.DatabaseClient -import org.springframework.r2dbc.core.bind -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional -import reactor.core.publisher.Mono -import java.net.URI -import java.util.UUID - -@Service -class SubjectAccessRightsService( - private val databaseClient: DatabaseClient, - private val r2dbcEntityTemplate: R2dbcEntityTemplate, -) { - - private val logger = LoggerFactory.getLogger(javaClass) - - @Transactional - fun create(subjectAccessRights: SubjectAccessRights): Mono = - databaseClient.sql( - """ - INSERT INTO subject_access_rights - (subject_id, subject_type, global_role, allowed_read_entities, allowed_write_entities) - VALUES (:subject_id, :subject_type, :global_role, :allowed_read_entities, :allowed_write_entities) - """ - ) - .bind("subject_id", subjectAccessRights.subjectId) - .bind("subject_type", subjectAccessRights.subjectType.toString()) - .bind("global_role", subjectAccessRights.globalRole) - .bind("allowed_read_entities", subjectAccessRights.allowedReadEntities) - .bind("allowed_write_entities", subjectAccessRights.allowedWriteEntities) - .fetch() - .rowsUpdated() - .thenReturn(1) - .onErrorResume { - logger.error("Error while creating a new subject access right : ${it.message}", it) - Mono.just(-1) - } - - fun retrieve(subjectId: UUID): Mono = - databaseClient.sql( - """ - SELECT * - FROM subject_access_rights - WHERE subject_id = :subject_id - """ - ) - .bind("subject_id", subjectId) - .fetch() - .one() - .map { rowToUserAccessRights(it) } - - @Transactional - fun addReadRoleOnEntity(subjectId: UUID, entityId: URI): Mono = - databaseClient.sql( - """ - UPDATE subject_access_rights - SET allowed_read_entities = array_append(allowed_read_entities, :entity_id::text) - WHERE subject_id = :subject_id - """ - ) - .bind("subject_id", subjectId) - .bind("entity_id", entityId) - .fetch() - .rowsUpdated() - .thenReturn(1) - .onErrorReturn(-1) - - @Transactional - fun addWriteRoleOnEntity(subjectId: UUID, entityId: URI): Mono = - databaseClient.sql( - """ - UPDATE subject_access_rights - SET allowed_write_entities = array_append(allowed_write_entities, :entity_id::text) - WHERE subject_id = :subject_id - """ - ) - .bind("subject_id", subjectId) - .bind("entity_id", entityId) - .fetch() - .rowsUpdated() - .thenReturn(1) - .onErrorReturn(-1) - - @Transactional - fun removeRoleOnEntity(subjectId: UUID, entityId: URI): Mono = - databaseClient.sql( - """ - UPDATE subject_access_rights - SET allowed_read_entities = array_remove(allowed_read_entities, :entity_id::text), - allowed_write_entities = array_remove(allowed_write_entities, :entity_id::text) - WHERE subject_id = :subject_id - """ - ) - .bind("subject_id", subjectId) - .bind("entity_id", entityId) - .fetch() - .rowsUpdated() - .thenReturn(1) - .onErrorReturn(-1) - - @Transactional - fun addAdminGlobalRole(subjectId: UUID): Mono = - databaseClient.sql( - """ - UPDATE subject_access_rights - SET global_role = '$ADMIN_ROLE_LABEL' - WHERE subject_id = :subject_id - """ - ) - .bind("subject_id", subjectId) - .fetch() - .rowsUpdated() - .thenReturn(1) - .onErrorReturn(-1) - - @Transactional - fun removeAdminGlobalRole(subjectId: UUID): Mono = - databaseClient.sql( - """ - UPDATE subject_access_rights - SET global_role = null - WHERE subject_id = :subject_id - """ - ) - .bind("subject_id", subjectId) - .fetch() - .rowsUpdated() - .thenReturn(1) - .onErrorReturn(-1) - - fun hasReadRoleOnEntity(subjectId: UUID, entityId: URI): Mono = - databaseClient.sql( - """ - SELECT COUNT(subject_id) as count - FROM subject_access_rights - WHERE subject_id = :subject_id - AND (:entity_id = ANY(allowed_read_entities) OR :entity_id = ANY(allowed_write_entities)) - """ - ) - .bind("subject_id", subjectId) - .bind("entity_id", entityId) - .fetch() - .one() - .map { - it["count"] as Long == 1L - } - .onErrorReturn(false) - - fun hasWriteRoleOnEntity(subjectId: UUID, entityId: URI): Mono = - databaseClient.sql( - """ - SELECT COUNT(subject_id) as count - FROM subject_access_rights - WHERE subject_id = :subject_id - AND (:entity_id = ANY(allowed_write_entities)) - """ - ) - .bind("subject_id", subjectId) - .bind("entity_id", entityId) - .fetch() - .one() - .map { - it["count"] as Long == 1L - } - .onErrorReturn(false) - - @Transactional - fun delete(subjectId: UUID): Mono = - r2dbcEntityTemplate.delete(SubjectAccessRights::class.java) - .matching(Query.query(Criteria.where("subject_id").`is`(subjectId))) - .all() - - private fun rowToUserAccessRights(row: Map) = - SubjectAccessRights( - subjectId = row["subject_id"] as UUID, - subjectType = SubjectType.valueOf(row["subject_type"] as String), - globalRole = row["global_role"] as String?, - allowedReadEntities = (row["allowed_read_entities"] as Array?), - allowedWriteEntities = (row["allowed_write_entities"] as Array?) - ) -} diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt new file mode 100644 index 000000000..ebaae0d5f --- /dev/null +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt @@ -0,0 +1,113 @@ +package com.egm.stellio.search.service + +import com.egm.stellio.search.model.SubjectReferential +import com.egm.stellio.shared.util.GlobalRole +import com.egm.stellio.shared.util.SubjectType +import org.slf4j.LoggerFactory +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate +import org.springframework.data.relational.core.query.Criteria +import org.springframework.data.relational.core.query.Query +import org.springframework.r2dbc.core.DatabaseClient +import org.springframework.r2dbc.core.bind +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import reactor.core.publisher.Mono +import java.util.UUID + +@Service +class SubjectReferentialService( + private val databaseClient: DatabaseClient, + private val r2dbcEntityTemplate: R2dbcEntityTemplate, +) { + + private val logger = LoggerFactory.getLogger(javaClass) + + @Transactional + fun create(subjectReferential: SubjectReferential): Mono = + databaseClient + .sql( + """ + INSERT INTO subject_referential + (subject_id, subject_type, global_roles, groups_memberships) + VALUES (:subject_id, :subject_type, :global_roles, :groups_memberships) + """ + ) + .bind("subject_id", subjectReferential.subjectId) + .bind("subject_type", subjectReferential.subjectType.toString()) + .bind("global_roles", subjectReferential.globalRoles?.map { it.toString() }?.toTypedArray()) + .bind("groups_memberships", subjectReferential.groupsMemberships?.toTypedArray()) + .fetch() + .rowsUpdated() + .thenReturn(1) + .onErrorResume { + logger.error("Error while creating a new subject referential : ${it.message}", it) + Mono.just(-1) + } + + fun retrieve(subjectId: UUID): Mono = + databaseClient + .sql( + """ + SELECT * + FROM subject_referential + WHERE subject_id = :subject_id + """ + ) + .bind("subject_id", subjectId) + .fetch() + .one() + .map { rowToUserAccessRights(it) } + + @Transactional + fun setGlobalRoles(subjectId: UUID, newRoles: List): Mono = + databaseClient + .sql( + """ + UPDATE subject_referential + SET global_roles = :global_roles + WHERE subject_id = :subject_id + """ + ) + .bind("subject_id", subjectId) + .bind("global_roles", newRoles.map { it.toString() }.toTypedArray()) + .fetch() + .rowsUpdated() + .thenReturn(1) + .onErrorResume { + logger.error("Error while setting global roles: $it") + Mono.just(-1) + } + + @Transactional + fun resetGlobalRoles(subjectId: UUID): Mono = + databaseClient + .sql( + """ + UPDATE subject_referential + SET global_roles = null + WHERE subject_id = :subject_id + """ + ) + .bind("subject_id", subjectId) + .fetch() + .rowsUpdated() + .thenReturn(1) + .onErrorResume { + logger.error("Error while resetting global roles: $it") + Mono.just(-1) + } + + @Transactional + fun delete(subjectId: UUID): Mono = + r2dbcEntityTemplate.delete(SubjectReferential::class.java) + .matching(Query.query(Criteria.where("subject_id").`is`(subjectId))) + .all() + + private fun rowToUserAccessRights(row: Map) = + SubjectReferential( + subjectId = row["subject_id"] as UUID, + subjectType = SubjectType.valueOf(row["subject_type"] as String), + globalRoles = (row["global_roles"] as Array?)?.map { GlobalRole.valueOf(it) }, + groupsMemberships = (row["groups_memberships"] as Array?)?.toList() + ) +} diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt index f73e6bfe5..47592c8b2 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt @@ -7,8 +7,8 @@ import arrow.core.left import arrow.core.right import com.egm.stellio.search.model.TemporalQuery import com.egm.stellio.search.service.AttributeInstanceService +import com.egm.stellio.search.service.EntityAccessRightsService import com.egm.stellio.search.service.QueryService -import com.egm.stellio.search.service.SubjectAccessRightsService import com.egm.stellio.search.service.TemporalEntityAttributeService import com.egm.stellio.shared.model.AccessDeniedException import com.egm.stellio.shared.model.BadRequestDataException @@ -46,7 +46,7 @@ class TemporalEntityHandler( private val attributeInstanceService: AttributeInstanceService, private val temporalEntityAttributeService: TemporalEntityAttributeService, private val queryService: QueryService, - private val subjectAccessRightsService: SubjectAccessRightsService + private val entityAccessRightsService: EntityAccessRightsService ) { /** @@ -60,7 +60,7 @@ class TemporalEntityHandler( ): ResponseEntity<*> { val userId = extractSubjectOrEmpty().awaitFirst() val canWriteEntity = - subjectAccessRightsService.hasWriteRoleOnEntity(UUID.fromString(userId), entityId.toUri()).awaitFirst() + entityAccessRightsService.hasWriteRoleOnEntity(UUID.fromString(userId), entityId.toUri()).awaitFirst() if (!canWriteEntity) throw AccessDeniedException("User forbidden write access to entity $entityId") @@ -145,7 +145,7 @@ class TemporalEntityHandler( val userId = extractSubjectOrEmpty().awaitFirst() val canReadEntity = - subjectAccessRightsService.hasReadRoleOnEntity(UUID.fromString(userId), entityId.toUri()).awaitFirst() + entityAccessRightsService.hasReadRoleOnEntity(UUID.fromString(userId), entityId.toUri()).awaitFirst() if (!canReadEntity) throw AccessDeniedException("User forbidden read access to entity $entityId") diff --git a/search-service/src/main/resources/db/migration/V0_17__add_user_access_rights_table.sql b/search-service/src/main/resources/db/migration/V0_17__add_user_access_rights_table.sql index b59749d32..74890dbe0 100644 --- a/search-service/src/main/resources/db/migration/V0_17__add_user_access_rights_table.sql +++ b/search-service/src/main/resources/db/migration/V0_17__add_user_access_rights_table.sql @@ -1,7 +1,15 @@ -CREATE TABLE subject_access_rights( +CREATE TABLE subject_referential( subject_id UUID NOT NULL PRIMARY KEY, subject_type VARCHAR(64) NOT NULL, - global_role VARCHAR(64), - allowed_read_entities TEXT[], - allowed_write_entities TEXT[] -) + global_roles TEXT[], + groups_memberships TEXT[] +); + +CREATE TABLE entity_access_rights( + subject_id UUID NOT NULL, + access_right VARCHAR(64) NOT NULL, + entity_id VARCHAR(255) NOT NULL +); + +ALTER TABLE entity_access_rights + ADD CONSTRAINT entity_access_rights_uniqueness UNIQUE (subject_id, access_right, entity_id); diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityAccessRightsServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityAccessRightsServiceTests.kt new file mode 100644 index 000000000..fe56e81ae --- /dev/null +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityAccessRightsServiceTests.kt @@ -0,0 +1,103 @@ +package com.egm.stellio.search.service + +import com.egm.stellio.search.model.EntityAccessRights +import com.egm.stellio.search.support.WithTimescaleContainer +import com.egm.stellio.shared.util.toUri +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate +import org.springframework.test.context.ActiveProfiles +import reactor.test.StepVerifier +import java.util.UUID + +@SpringBootTest +@ActiveProfiles("test") +class EntityAccessRightsServiceTests : WithTimescaleContainer { + + @Autowired + private lateinit var entityAccessRightsService: EntityAccessRightsService + + @Autowired + private lateinit var r2dbcEntityTemplate: R2dbcEntityTemplate + + private val subjectUuid = UUID.fromString("0768A6D5-D87B-4209-9A22-8C40A8961A79") + private val entityId = "urn:ngsi-ld:Entity:1111".toUri() + + @AfterEach + fun clearEntityAccessRightsTable() { + r2dbcEntityTemplate.delete(EntityAccessRights::class.java) + .all() + .block() + } + + @Test + fun `it should add a new entity in the allowed list of read entities`() { + StepVerifier + .create(entityAccessRightsService.setReadRoleOnEntity(subjectUuid, entityId)) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + + StepVerifier + .create(entityAccessRightsService.hasReadRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:1111".toUri())) + .expectNextMatches { it == true } + .expectComplete() + .verify() + } + + @Test + fun `it should remove an entity from the allowed list of read entities`() { + StepVerifier + .create(entityAccessRightsService.setReadRoleOnEntity(subjectUuid, entityId)) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + + StepVerifier + .create(entityAccessRightsService.removeRoleOnEntity(subjectUuid, entityId)) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + + StepVerifier + .create(entityAccessRightsService.hasReadRoleOnEntity(subjectUuid, entityId)) + .expectNextMatches { it == false } + .expectComplete() + .verify() + } + + @Test + fun `it should find if an user has a read role on a entity`() { + StepVerifier + .create(entityAccessRightsService.setReadRoleOnEntity(subjectUuid, entityId)) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + + StepVerifier + .create(entityAccessRightsService.setWriteRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:6666".toUri())) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + + StepVerifier + .create(entityAccessRightsService.hasReadRoleOnEntity(subjectUuid, entityId)) + .expectNextMatches { it == true } + .expectComplete() + .verify() + + StepVerifier + .create(entityAccessRightsService.hasReadRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:2222".toUri())) + .expectNextMatches { it == false } + .expectComplete() + .verify() + + StepVerifier + .create(entityAccessRightsService.hasReadRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:6666".toUri())) + .expectNextMatches { it == true } + .expectComplete() + .verify() + } +} diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt index ae3a805b0..eb003eac4 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt @@ -1,5 +1,7 @@ package com.egm.stellio.search.service +import com.egm.stellio.shared.util.AccessRight +import com.egm.stellio.shared.util.GlobalRole import com.egm.stellio.shared.util.SubjectType import com.egm.stellio.shared.util.loadSampleData import com.egm.stellio.shared.util.toUUID @@ -20,7 +22,10 @@ class IAMListenerTests { private lateinit var iamListener: IAMListener @MockkBean(relaxed = true) - private lateinit var subjectAccessRightsService: SubjectAccessRightsService + private lateinit var subjectReferentialService: SubjectReferentialService + + @MockkBean(relaxed = true) + private lateinit var entityAccessRightsService: EntityAccessRightsService @Test fun `it should handle a create event for a subject`() { @@ -29,13 +34,11 @@ class IAMListenerTests { iamListener.processMessage(subjectCreateEvent) verify { - subjectAccessRightsService.create( + subjectReferentialService.create( match { it.subjectId == "6ad19fe0-fc11-4024-85f2-931c6fa6f7e0".toUUID() && it.subjectType == SubjectType.USER && - it.globalRole == null && - it.allowedReadEntities.isNullOrEmpty() && - it.allowedWriteEntities.isNullOrEmpty() + it.globalRoles == null } ) } @@ -49,7 +52,7 @@ class IAMListenerTests { iamListener.processMessage(subjectDeleteEvent) verify { - subjectAccessRightsService.delete( + subjectReferentialService.delete( match { it == "6ad19fe0-fc11-4024-85f2-931c6fa6f7e0".toUUID() } @@ -65,10 +68,11 @@ class IAMListenerTests { iamListener.processMessage(roleAppendEvent) verify { - subjectAccessRightsService.addAdminGlobalRole( + subjectReferentialService.setGlobalRoles( match { it == "ab67edf3-238c-4f50-83f4-617c620c62eb".toUUID() - } + }, + eq(listOf(GlobalRole.STELLIO_ADMIN)) ) } confirmVerified() @@ -81,10 +85,11 @@ class IAMListenerTests { iamListener.processMessage(roleAppendEvent) verify { - subjectAccessRightsService.addAdminGlobalRole( + subjectReferentialService.setGlobalRoles( match { it == "ab67edf3-238c-4f50-83f4-617c620c62eb".toUUID() - } + }, + eq(listOf(GlobalRole.STELLIO_ADMIN)) ) } confirmVerified() @@ -97,10 +102,11 @@ class IAMListenerTests { iamListener.processMessage(roleAppendEvent) verify { - subjectAccessRightsService.addAdminGlobalRole( + subjectReferentialService.setGlobalRoles( match { it == "ab67edf3-238c-4f50-83f4-617c620c62eb".toUUID() - } + }, + eq(listOf(GlobalRole.STELLIO_ADMIN, GlobalRole.STELLIO_CREATOR)) ) } confirmVerified() @@ -113,7 +119,7 @@ class IAMListenerTests { iamListener.processMessage(roleAppendEvent) verify { - subjectAccessRightsService.removeAdminGlobalRole( + subjectReferentialService.resetGlobalRoles( match { it == "ab67edf3-238c-4f50-83f4-617c620c62eb".toUUID() } @@ -129,22 +135,23 @@ class IAMListenerTests { iamListener.processIamRights(rightAppendEvent) verify { - subjectAccessRightsService.addReadRoleOnEntity( + entityAccessRightsService.setRoleOnEntity( eq("312b30b4-9279-4f7e-bdc5-ec56d699bb7d".toUUID()), - eq("urn:ngsi-ld:Beekeeper:01".toUri()) + eq("urn:ngsi-ld:Beekeeper:01".toUri()), + eq(AccessRight.R_CAN_READ) ) } confirmVerified() } @Test - fun `it should handle an delete event removing a right on an entity`() { + fun `it should handle a delete event removing a right on an entity`() { val rightRemoveEvent = loadSampleData("events/authorization/RightRemoveOnEntity.json") iamListener.processIamRights(rightRemoveEvent) verify { - subjectAccessRightsService.removeRoleOnEntity( + entityAccessRightsService.removeRoleOnEntity( eq("312b30b4-9279-4f7e-bdc5-ec56d699bb7d".toUUID()), eq("urn:ngsi-ld:Beekeeper:01".toUri()) ) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt deleted file mode 100644 index 3effac620..000000000 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectAccessRightsServiceTests.kt +++ /dev/null @@ -1,237 +0,0 @@ -package com.egm.stellio.search.service - -import com.egm.stellio.search.model.SubjectAccessRights -import com.egm.stellio.search.support.WithTimescaleContainer -import com.egm.stellio.shared.util.ADMIN_ROLE_LABEL -import com.egm.stellio.shared.util.SubjectType -import com.egm.stellio.shared.util.toUri -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.data.r2dbc.core.R2dbcEntityTemplate -import org.springframework.test.context.ActiveProfiles -import reactor.test.StepVerifier -import java.util.UUID - -@SpringBootTest -@ActiveProfiles("test") -class SubjectAccessRightsServiceTests : WithTimescaleContainer { - - @Autowired - private lateinit var subjectAccessRightsService: SubjectAccessRightsService - - @Autowired - private lateinit var r2dbcEntityTemplate: R2dbcEntityTemplate - - private val subjectUuid = UUID.fromString("0768A6D5-D87B-4209-9A22-8C40A8961A79") - - @AfterEach - fun clearUsersAccessRightsTable() { - r2dbcEntityTemplate.delete(SubjectAccessRights::class.java) - .all() - .block() - } - - @Test - fun `it should persist an user access right`() { - val userAccessRights = SubjectAccessRights( - subjectId = subjectUuid, - subjectType = SubjectType.USER, - globalRole = ADMIN_ROLE_LABEL, - allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678"), - allowedWriteEntities = arrayOf("urn:ngsi-ld:Entity:6666", "urn:ngsi-ld:Entity:0000") - ) - - StepVerifier.create( - subjectAccessRightsService.create(userAccessRights) - ) - .expectNextMatches { it == 1 } - .expectComplete() - .verify() - } - - @Test - fun `it should retrieve an user access right`() { - val allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678") - val allowedWriteEntities = arrayOf("urn:ngsi-ld:Entity:6666", "urn:ngsi-ld:Entity:0000") - val userAccessRights = SubjectAccessRights( - subjectId = subjectUuid, - subjectType = SubjectType.USER, - globalRole = ADMIN_ROLE_LABEL, - allowedReadEntities = allowedReadEntities, - allowedWriteEntities = allowedWriteEntities - ) - - subjectAccessRightsService.create(userAccessRights).block() - - StepVerifier.create( - subjectAccessRightsService.retrieve(subjectUuid) - ) - .expectNextMatches { - it.subjectId == subjectUuid && - it.subjectType == SubjectType.USER && - it.globalRole == ADMIN_ROLE_LABEL && - it.allowedReadEntities?.contentEquals(allowedReadEntities) == true && - it.allowedWriteEntities?.contentEquals(allowedWriteEntities) == true - } - .expectComplete() - .verify() - } - - @Test - fun `it should add a new entity in the allowed list of read entities`() { - val userAccessRights = SubjectAccessRights( - subjectId = subjectUuid, - subjectType = SubjectType.USER, - globalRole = ADMIN_ROLE_LABEL, - allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678") - ) - - subjectAccessRightsService.create(userAccessRights).block() - - StepVerifier.create( - subjectAccessRightsService.addReadRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:1111".toUri()) - ) - .expectNextMatches { it == 1 } - .expectComplete() - .verify() - - StepVerifier.create( - subjectAccessRightsService.hasReadRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:1111".toUri()) - ) - .expectNextMatches { it == true } - .expectComplete() - .verify() - } - - @Test - fun `it should remove an entity from the allowed list of read entities`() { - val userAccessRights = SubjectAccessRights( - subjectId = subjectUuid, - subjectType = SubjectType.USER, - globalRole = ADMIN_ROLE_LABEL, - allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678") - ) - - subjectAccessRightsService.create(userAccessRights).block() - - StepVerifier.create( - subjectAccessRightsService.removeRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:1234".toUri()) - ) - .expectNextMatches { it == 1 } - .expectComplete() - .verify() - - StepVerifier.create( - subjectAccessRightsService.hasReadRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:1234".toUri()) - ) - .expectNextMatches { it == false } - .expectComplete() - .verify() - } - - @Test - fun `it should update the global role of a subject`() { - val userAccessRights = SubjectAccessRights( - subjectId = subjectUuid, - subjectType = SubjectType.USER, - allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678"), - allowedWriteEntities = arrayOf("urn:ngsi-ld:Entity:6666") - ) - - subjectAccessRightsService.create(userAccessRights).block() - - StepVerifier.create( - subjectAccessRightsService.addAdminGlobalRole(subjectUuid) - ) - .expectNextMatches { it == 1 } - .expectComplete() - .verify() - - StepVerifier.create( - subjectAccessRightsService.retrieve(subjectUuid) - ) - .expectNextMatches { - it.globalRole == ADMIN_ROLE_LABEL - } - .expectComplete() - .verify() - - StepVerifier.create( - subjectAccessRightsService.removeAdminGlobalRole(subjectUuid) - ) - .expectNextMatches { it == 1 } - .expectComplete() - .verify() - - StepVerifier.create( - subjectAccessRightsService.retrieve(subjectUuid) - ) - .expectNextMatches { - it.globalRole == null - } - .expectComplete() - .verify() - } - - @Test - fun `it should find if an user has a read role on a entity`() { - val userAccessRights = SubjectAccessRights( - subjectId = subjectUuid, - subjectType = SubjectType.USER, - globalRole = ADMIN_ROLE_LABEL, - allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678"), - allowedWriteEntities = arrayOf("urn:ngsi-ld:Entity:6666") - ) - - subjectAccessRightsService.create(userAccessRights).block() - - StepVerifier.create( - subjectAccessRightsService.hasReadRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:1234".toUri()) - ) - .expectNextMatches { it == true } - .expectComplete() - .verify() - - StepVerifier.create( - subjectAccessRightsService.hasReadRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:1111".toUri()) - ) - .expectNextMatches { it == false } - .expectComplete() - .verify() - - StepVerifier.create( - subjectAccessRightsService.hasReadRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:6666".toUri()) - ) - .expectNextMatches { it == true } - .expectComplete() - .verify() - } - - @Test - fun `it should delete an user access right`() { - val userAccessRights = SubjectAccessRights( - subjectId = subjectUuid, - subjectType = SubjectType.USER, - globalRole = ADMIN_ROLE_LABEL, - allowedReadEntities = arrayOf("urn:ngsi-ld:Entity:1234", "urn:ngsi-ld:Entity:5678"), - allowedWriteEntities = arrayOf("urn:ngsi-ld:Entity:6666", "urn:ngsi-ld:Entity:0000") - ) - - subjectAccessRightsService.create(userAccessRights).block() - - StepVerifier.create( - subjectAccessRightsService.delete(subjectUuid) - ) - .expectNextMatches { it == 1 } - .expectComplete() - .verify() - - StepVerifier.create( - subjectAccessRightsService.retrieve(subjectUuid) - ) - .expectComplete() - .verify() - } -} diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectReferentialServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectReferentialServiceTests.kt new file mode 100644 index 000000000..2ad3620bb --- /dev/null +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectReferentialServiceTests.kt @@ -0,0 +1,130 @@ +package com.egm.stellio.search.service + +import com.egm.stellio.search.model.SubjectReferential +import com.egm.stellio.search.support.WithTimescaleContainer +import com.egm.stellio.shared.util.GlobalRole +import com.egm.stellio.shared.util.SubjectType +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate +import org.springframework.test.context.ActiveProfiles +import reactor.test.StepVerifier +import java.util.UUID + +@SpringBootTest +@ActiveProfiles("test") +class SubjectReferentialServiceTests : WithTimescaleContainer { + + @Autowired + private lateinit var subjectReferentialService: SubjectReferentialService + + @Autowired + private lateinit var r2dbcEntityTemplate: R2dbcEntityTemplate + + private val subjectUuid = UUID.fromString("0768A6D5-D87B-4209-9A22-8C40A8961A79") + + @AfterEach + fun clearSubjectReferentialTable() { + r2dbcEntityTemplate.delete(SubjectReferential::class.java) + .all() + .block() + } + + @Test + fun `it should persist a subject referential`() { + val subjectReferential = SubjectReferential( + subjectId = subjectUuid, + subjectType = SubjectType.USER, + globalRoles = listOf(GlobalRole.STELLIO_ADMIN) + ) + + StepVerifier + .create(subjectReferentialService.create(subjectReferential)) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + } + + @Test + fun `it should retrieve a subject referential`() { + val subjectReferential = SubjectReferential( + subjectId = subjectUuid, + subjectType = SubjectType.USER, + globalRoles = listOf(GlobalRole.STELLIO_ADMIN) + ) + + subjectReferentialService.create(subjectReferential).block() + + StepVerifier + .create(subjectReferentialService.retrieve(subjectUuid)) + .expectNextMatches { + it.subjectId == subjectUuid && + it.subjectType == SubjectType.USER && + it.globalRoles == listOf(GlobalRole.STELLIO_ADMIN) + } + .expectComplete() + .verify() + } + + @Test + fun `it should update the global role of a subject`() { + val subjectReferential = SubjectReferential( + subjectId = subjectUuid, + subjectType = SubjectType.USER + ) + + subjectReferentialService.create(subjectReferential).block() + + StepVerifier + .create(subjectReferentialService.setGlobalRoles(subjectUuid, listOf(GlobalRole.STELLIO_ADMIN))) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + + StepVerifier + .create(subjectReferentialService.retrieve(subjectUuid)) + .expectNextMatches { + it.globalRoles == listOf(GlobalRole.STELLIO_ADMIN) + } + .expectComplete() + .verify() + + StepVerifier + .create(subjectReferentialService.resetGlobalRoles(subjectUuid)) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + + StepVerifier + .create(subjectReferentialService.retrieve(subjectUuid)) + .expectNextMatches { + it.globalRoles == null + } + .expectComplete() + .verify() + } + + @Test + fun `it should delete a subject referential`() { + val userAccessRights = SubjectReferential( + subjectId = subjectUuid, + subjectType = SubjectType.USER, + globalRoles = listOf(GlobalRole.STELLIO_ADMIN) + ) + + subjectReferentialService.create(userAccessRights).block() + + StepVerifier + .create(subjectReferentialService.delete(subjectUuid)) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + + StepVerifier + .create(subjectReferentialService.retrieve(subjectUuid)) + .expectComplete() + .verify() + } +} diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt index e21fd0023..99a96cbb9 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt @@ -6,8 +6,8 @@ import com.egm.stellio.search.model.TemporalEntitiesQuery import com.egm.stellio.search.model.TemporalEntityAttribute import com.egm.stellio.search.model.TemporalQuery import com.egm.stellio.search.service.AttributeInstanceService +import com.egm.stellio.search.service.EntityAccessRightsService import com.egm.stellio.search.service.QueryService -import com.egm.stellio.search.service.SubjectAccessRightsService import com.egm.stellio.search.service.TemporalEntityAttributeService import com.egm.stellio.shared.WithMockCustomUser import com.egm.stellio.shared.model.BadRequestDataException @@ -70,7 +70,7 @@ class TemporalEntityHandlerTests { private lateinit var temporalEntityAttributeService: TemporalEntityAttributeService @MockkBean - private lateinit var subjectAccessRightsService: SubjectAccessRightsService + private lateinit var entityAccessRightsService: EntityAccessRightsService private val entityUri = "urn:ngsi-ld:BeeHive:TESTC".toUri() @@ -92,7 +92,7 @@ class TemporalEntityHandlerTests { loadSampleData("fragments/temporal_entity_fragment_one_attribute_one_instance.jsonld") val temporalEntityAttributeUuid = UUID.randomUUID() - every { subjectAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } every { temporalEntityAttributeService.getForEntityAndAttribute(any(), any()) } answers { Mono.just(temporalEntityAttributeUuid) } @@ -134,7 +134,7 @@ class TemporalEntityHandlerTests { loadSampleData("fragments/temporal_entity_fragment_one_attribute_many_instances.jsonld") val temporalEntityAttributeUuid = UUID.randomUUID() - every { subjectAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } every { temporalEntityAttributeService.getForEntityAndAttribute(any(), any()) } answers { Mono.just(temporalEntityAttributeUuid) } @@ -176,7 +176,7 @@ class TemporalEntityHandlerTests { loadSampleData("fragments/temporal_entity_fragment_many_attributes_one_instance.jsonld") val temporalEntityAttributeUuid = UUID.randomUUID() - every { subjectAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } every { temporalEntityAttributeService.getForEntityAndAttribute(any(), any()) } answers { Mono.just(temporalEntityAttributeUuid) } @@ -221,7 +221,7 @@ class TemporalEntityHandlerTests { loadSampleData("fragments/temporal_entity_fragment_many_attributes_many_instances.jsonld") val temporalEntityAttributeUuid = UUID.randomUUID() - every { subjectAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } every { temporalEntityAttributeService.getForEntityAndAttribute(any(), any()) } answers { Mono.just(temporalEntityAttributeUuid) } @@ -262,7 +262,7 @@ class TemporalEntityHandlerTests { @Test fun `it should return a 400 if temporal entity fragment is badly formed`() { - every { subjectAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } webClient.post() .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs") @@ -284,7 +284,7 @@ class TemporalEntityHandlerTests { @Test fun `it should return a 403 is user is not authorized to write on the entity`() { - every { subjectAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(false) } + every { entityAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(false) } webClient.post() .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs") @@ -302,7 +302,7 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if timerel is present without time query param`() { - every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=before") @@ -321,7 +321,7 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if time is present without timerel query param`() { - every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?time=2020-10-29T18:00:00Z") @@ -341,7 +341,7 @@ class TemporalEntityHandlerTests { @Test fun `it should give a 200 if no timerel and no time query params are in the request`() { coEvery { queryService.queryTemporalEntity(any(), any(), any(), any()) } returns emptyMap() - every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri("/ngsi-ld/v1/temporal/entities/$entityUri") @@ -351,7 +351,7 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if timerel is between and no endTime provided`() { - every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=between&time=startTime") @@ -370,7 +370,7 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if time is not parsable`() { - every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=before&time=badTime") @@ -389,7 +389,7 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if timerel is not a valid value`() { - every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=befor&time=badTime") @@ -408,7 +408,7 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if timerel is between and endTime is not parseable`() { - every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri( @@ -430,7 +430,7 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if one of time bucket or aggregate is missing`() { - every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri( @@ -452,7 +452,7 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if aggregate function is unknown`() { - every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri( @@ -474,7 +474,7 @@ class TemporalEntityHandlerTests { @Test fun `it should return a 404 if temporal entity attribute does not exist`() { - every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } coEvery { queryService.queryTemporalEntity(any(), any(), any(), any()) } throws ResourceNotFoundException("Entity urn:ngsi-ld:BeeHive:TESTC was not found") @@ -499,8 +499,8 @@ class TemporalEntityHandlerTests { @Test fun `it should return a 200 if minimal required parameters are valid`() { + every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } coEvery { queryService.queryTemporalEntity(any(), any(), any(), any()) } returns emptyMap() - every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri( @@ -529,7 +529,7 @@ class TemporalEntityHandlerTests { @Test fun `it should return an entity with two temporal properties evolution`() { mockWithIncomingAndOutgoingTemporalProperties(false) - every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri( @@ -550,7 +550,7 @@ class TemporalEntityHandlerTests { @Test fun `it should return a json entity with two temporal properties evolution`() { mockWithIncomingAndOutgoingTemporalProperties(false) - every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri( @@ -573,7 +573,7 @@ class TemporalEntityHandlerTests { @Test fun `it should return an entity with two temporal properties evolution with temporalValues option`() { mockWithIncomingAndOutgoingTemporalProperties(true) - every { subjectAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri( diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt index e6bbb9c17..bb3dd7c06 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt @@ -1,5 +1,7 @@ package com.egm.stellio.shared.util +import com.egm.stellio.shared.util.GlobalRole.STELLIO_ADMIN +import com.egm.stellio.shared.util.GlobalRole.STELLIO_CREATOR import org.springframework.security.core.context.ReactiveSecurityContextHolder import org.springframework.security.core.context.SecurityContextImpl import org.springframework.security.oauth2.jwt.Jwt @@ -7,10 +9,8 @@ import reactor.core.publisher.Mono import java.net.URI import java.util.UUID -const val ADMIN_ROLE_LABEL = "stellio-admin" -const val CREATION_ROLE_LABEL = "stellio-creator" -val ADMIN_ROLES: Set = setOf(ADMIN_ROLE_LABEL) -val CREATION_ROLES: Set = setOf(CREATION_ROLE_LABEL).plus(ADMIN_ROLES) +val ADMIN_ROLES: Set = setOf(STELLIO_ADMIN) +val CREATION_ROLES: Set = setOf(STELLIO_CREATOR).plus(ADMIN_ROLES) fun extractSubjectOrEmpty(): Mono { return ReactiveSecurityContextHolder.getContext() @@ -29,3 +29,26 @@ enum class SubjectType { GROUP, CLIENT } + +enum class GlobalRole(val key: String) { + STELLIO_CREATOR("stellio-creator"), + STELLIO_ADMIN("stellio-admin"); + + companion object { + fun forKey(key: String): GlobalRole = + values().find { it.key == key } ?: throw IllegalArgumentException("Unrecognized key $key") + } +} + +enum class AccessRight(val attributeName: String) { + R_CAN_READ("rCanRead"), + R_CAN_WRITE("rCanWrite"), + R_CAN_ADMIN("rCanAdmin"); + + companion object { + fun forAttributeName(attributeName: String): AccessRight = + values().find { + it.attributeName == attributeName + } ?: throw IllegalArgumentException("Unrecognized attribute name $attributeName") + } +} From 9f55df6766e3b7aac0e31524d0503d9493d8f2fe Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Thu, 9 Dec 2021 19:01:23 +0100 Subject: [PATCH 17/28] feat(search): add support for attribute append and delete events from cim.iam --- .../search/model/SubjectReferential.kt | 1 + .../egm/stellio/search/service/IAMListener.kt | 34 ++++-- .../service/SubjectReferentialService.kt | 61 ++++++++++- .../V0_17__add_user_access_rights_table.sql | 1 + .../search/service/IAMListenerTests.kt | 57 ++++++++++ .../service/SubjectReferentialServiceTests.kt | 101 ++++++++++++++++++ .../com/egm/stellio/shared/util/AuthUtils.kt | 3 + .../ServiceAccountIdAppendEvent.json | 12 +++ 8 files changed, 262 insertions(+), 8 deletions(-) create mode 100644 shared/src/testFixtures/resources/ngsild/events/authorization/ServiceAccountIdAppendEvent.json diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectReferential.kt b/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectReferential.kt index bcbd600f2..f22fd168a 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectReferential.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/model/SubjectReferential.kt @@ -9,6 +9,7 @@ data class SubjectReferential( @Id val subjectId: UUID, val subjectType: SubjectType, + val serviceAccountId: UUID? = null, val globalRoles: List? = null, val groupsMemberships: List? = null ) { diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt index fc05f89f9..5c35991f4 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt @@ -32,9 +32,9 @@ class IAMListener( when (val authorizationEvent = JsonUtils.deserializeAs(content)) { is EntityCreateEvent -> createSubjectReferential(authorizationEvent) is EntityDeleteEvent -> deleteSubjectReferential(authorizationEvent) - is AttributeAppendEvent -> addRoleToSubject(authorizationEvent) - is AttributeReplaceEvent -> TODO() - is AttributeDeleteEvent -> TODO() + is AttributeAppendEvent -> updateSubjectProfile(authorizationEvent) + is AttributeReplaceEvent -> logger.debug("Not interested in attribute replace events for IAM events") + is AttributeDeleteEvent -> removeSubjectFromGroup(authorizationEvent) else -> logger.info("Authorization event ${authorizationEvent.operationType} not handled.") } } @@ -67,20 +67,40 @@ class IAMListener( } } - private fun addRoleToSubject(attributeAppendEvent: AttributeAppendEvent) { + private fun updateSubjectProfile(attributeAppendEvent: AttributeAppendEvent) { + val operationPayloadNode = jacksonObjectMapper().readTree(attributeAppendEvent.operationPayload) + val subjectUuid = attributeAppendEvent.entityId.extractSubjectUuid() if (attributeAppendEvent.attributeName == "roles") { - val operationPayloadNode = jacksonObjectMapper().readTree(attributeAppendEvent.operationPayload) val updatedRoles = (operationPayloadNode["value"] as ArrayNode).elements() val newRoles = updatedRoles.asSequence().map { GlobalRole.forKey(it.asText()) }.toList() if (newRoles.isNotEmpty()) - subjectReferentialService.setGlobalRoles(attributeAppendEvent.entityId.extractSubjectUuid(), newRoles) + subjectReferentialService.setGlobalRoles(subjectUuid, newRoles) else - subjectReferentialService.resetGlobalRoles(attributeAppendEvent.entityId.extractSubjectUuid()) + subjectReferentialService.resetGlobalRoles(subjectUuid) + } else if (attributeAppendEvent.attributeName == "serviceAccountId") { + val serviceAccountId = operationPayloadNode["value"].asText() + subjectReferentialService.addServiceAccountIdToClient( + subjectUuid, + serviceAccountId.extractSubjectUuid() + ) + } else if (attributeAppendEvent.attributeName == "isMemberOf") { + val groupId = operationPayloadNode["object"].asText() + subjectReferentialService.addGroupMembershipToUser( + subjectUuid, + groupId.extractSubjectUuid() + ) } } + private fun removeSubjectFromGroup(attributeDeleteEvent: AttributeDeleteEvent) { + subjectReferentialService.removeGroupMembershipToUser( + attributeDeleteEvent.entityId.extractSubjectUuid(), + attributeDeleteEvent.datasetId!!.extractSubjectUuid() + ) + } + private fun addEntityToSubject(attributeAppendEvent: AttributeAppendEvent) { val operationPayloadNode = jacksonObjectMapper().readTree(attributeAppendEvent.operationPayload) val entityId = operationPayloadNode["object"].asText() diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt index ebaae0d5f..50ea6ba38 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt @@ -3,6 +3,7 @@ package com.egm.stellio.search.service import com.egm.stellio.search.model.SubjectReferential import com.egm.stellio.shared.util.GlobalRole import com.egm.stellio.shared.util.SubjectType +import com.egm.stellio.shared.util.toUUID import org.slf4j.LoggerFactory import org.springframework.data.r2dbc.core.R2dbcEntityTemplate import org.springframework.data.relational.core.query.Criteria @@ -97,6 +98,63 @@ class SubjectReferentialService( Mono.just(-1) } + @Transactional + fun addGroupMembershipToUser(subjectId: UUID, groupId: UUID): Mono = + databaseClient + .sql( + """ + UPDATE subject_referential + SET groups_memberships = array_append(groups_memberships, :group_id::text) + WHERE subject_id = :subject_id + """.trimIndent() + ) + .bind("subject_id", subjectId) + .bind("group_id", groupId) + .fetch() + .rowsUpdated() + .onErrorResume { + logger.error("Error while adding group membership to user: $it") + Mono.just(-1) + } + + @Transactional + fun removeGroupMembershipToUser(subjectId: UUID, groupId: UUID): Mono = + databaseClient + .sql( + """ + UPDATE subject_referential + SET groups_memberships = array_remove(groups_memberships, :group_id::text) + WHERE subject_id = :subject_id + """.trimIndent() + ) + .bind("subject_id", subjectId) + .bind("group_id", groupId) + .fetch() + .rowsUpdated() + .onErrorResume { + logger.error("Error while removing group membership to user: $it") + Mono.just(-1) + } + + @Transactional + fun addServiceAccountIdToClient(subjectId: UUID, serviceAccountId: UUID): Mono = + databaseClient + .sql( + """ + UPDATE subject_referential + SET service_account_id = :service_account_id + WHERE subject_id = :subject_id + """.trimIndent() + ) + .bind("subject_id", subjectId) + .bind("service_account_id", serviceAccountId) + .fetch() + .rowsUpdated() + .onErrorResume { + logger.error("Error while setting service account id to client: $it") + Mono.just(-1) + } + @Transactional fun delete(subjectId: UUID): Mono = r2dbcEntityTemplate.delete(SubjectReferential::class.java) @@ -107,7 +165,8 @@ class SubjectReferentialService( SubjectReferential( subjectId = row["subject_id"] as UUID, subjectType = SubjectType.valueOf(row["subject_type"] as String), + serviceAccountId = row["service_account_id"] as UUID?, globalRoles = (row["global_roles"] as Array?)?.map { GlobalRole.valueOf(it) }, - groupsMemberships = (row["groups_memberships"] as Array?)?.toList() + groupsMemberships = (row["groups_memberships"] as Array?)?.map { it.toUUID() } ) } diff --git a/search-service/src/main/resources/db/migration/V0_17__add_user_access_rights_table.sql b/search-service/src/main/resources/db/migration/V0_17__add_user_access_rights_table.sql index 74890dbe0..a7b03f628 100644 --- a/search-service/src/main/resources/db/migration/V0_17__add_user_access_rights_table.sql +++ b/search-service/src/main/resources/db/migration/V0_17__add_user_access_rights_table.sql @@ -1,6 +1,7 @@ CREATE TABLE subject_referential( subject_id UUID NOT NULL PRIMARY KEY, subject_type VARCHAR(64) NOT NULL, + service_account_id UUID, global_roles TEXT[], groups_memberships TEXT[] ); diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt index eb003eac4..57b1e4ad2 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt @@ -128,6 +128,63 @@ class IAMListenerTests { confirmVerified() } + @Test + fun `it should handle an append event adding an user to a group`() { + val roleAppendEvent = loadSampleData("events/authorization/GroupMembershipAppendEvent.json") + + iamListener.processMessage(roleAppendEvent) + + verify { + subjectReferentialService.addGroupMembershipToUser( + match { + it == "96e1f1e9-d798-48d7-820e-59f5a9a2abf5".toUUID() + }, + match { + it == "7cdad168-96ee-4649-b768-a060ac2ef435".toUUID() + } + ) + } + confirmVerified() + } + + @Test + fun `it should handle a delete event removing an user from a group`() { + val roleAppendEvent = loadSampleData("events/authorization/GroupMembershipDeleteEvent.json") + + iamListener.processMessage(roleAppendEvent) + + verify { + subjectReferentialService.removeGroupMembershipToUser( + match { + it == "96e1f1e9-d798-48d7-820e-59f5a9a2abf5".toUUID() + }, + match { + it == "7cdad168-96ee-4649-b768-a060ac2ef435".toUUID() + } + ) + } + confirmVerified() + } + + @Test + fun `it should handle an append event adding a service account id to a client`() { + val roleAppendEvent = loadSampleData("events/authorization/ServiceAccountIdAppendEvent.json") + + iamListener.processMessage(roleAppendEvent) + + verify { + subjectReferentialService.addServiceAccountIdToClient( + match { + it == "96e1f1e9-d798-48d7-820e-59f5a9a2abf5".toUUID() + }, + match { + it == "7cdad168-96ee-4649-b768-a060ac2ef435".toUUID() + } + ) + } + confirmVerified() + } + @Test fun `it should handle an append event adding a right on an entity`() { val rightAppendEvent = loadSampleData("events/authorization/RightAddOnEntity.json") diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectReferentialServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectReferentialServiceTests.kt index 2ad3620bb..11481a8e5 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectReferentialServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectReferentialServiceTests.kt @@ -24,6 +24,7 @@ class SubjectReferentialServiceTests : WithTimescaleContainer { private lateinit var r2dbcEntityTemplate: R2dbcEntityTemplate private val subjectUuid = UUID.fromString("0768A6D5-D87B-4209-9A22-8C40A8961A79") + private val groupUuid = UUID.fromString("52A916AB-19E6-4D3B-B629-936BC8E5B640") @AfterEach fun clearSubjectReferentialTable() { @@ -106,6 +107,106 @@ class SubjectReferentialServiceTests : WithTimescaleContainer { .verify() } + @Test + fun `it should add a group membership to an user`() { + val userAccessRights = SubjectReferential( + subjectId = subjectUuid, + subjectType = SubjectType.USER + ) + + subjectReferentialService.create(userAccessRights).block() + + StepVerifier + .create(subjectReferentialService.addGroupMembershipToUser(subjectUuid, groupUuid)) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + + StepVerifier + .create(subjectReferentialService.retrieve(subjectUuid)) + .expectNextMatches { + it.groupsMemberships == listOf(groupUuid) + } + .expectComplete() + .verify() + } + + @Test + fun `it should add a group membership to an user inside an existing list`() { + val userAccessRights = SubjectReferential( + subjectId = subjectUuid, + subjectType = SubjectType.USER + ) + + subjectReferentialService.create(userAccessRights).block() + subjectReferentialService.addGroupMembershipToUser(subjectUuid, groupUuid).block() + + val newGroupUuid = UUID.randomUUID() + StepVerifier + .create(subjectReferentialService.addGroupMembershipToUser(subjectUuid, newGroupUuid)) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + + StepVerifier + .create(subjectReferentialService.retrieve(subjectUuid)) + .expectNextMatches { + it.groupsMemberships == listOf(groupUuid, newGroupUuid) + } + .expectComplete() + .verify() + } + + @Test + fun `it should remove a group membership to an user`() { + val userAccessRights = SubjectReferential( + subjectId = subjectUuid, + subjectType = SubjectType.USER + ) + + subjectReferentialService.create(userAccessRights).block() + subjectReferentialService.addGroupMembershipToUser(subjectUuid, groupUuid).block() + + StepVerifier + .create(subjectReferentialService.removeGroupMembershipToUser(subjectUuid, groupUuid)) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + + StepVerifier + .create(subjectReferentialService.retrieve(subjectUuid)) + .expectNextMatches { + it.groupsMemberships?.isEmpty() ?: false + } + .expectComplete() + .verify() + } + + @Test + fun `it should add a service account id to a client`() { + val userAccessRights = SubjectReferential( + subjectId = subjectUuid, + subjectType = SubjectType.USER + ) + + subjectReferentialService.create(userAccessRights).block() + + val serviceAccountId = UUID.randomUUID() + StepVerifier + .create(subjectReferentialService.addServiceAccountIdToClient(subjectUuid, serviceAccountId)) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + + StepVerifier + .create(subjectReferentialService.retrieve(subjectUuid)) + .expectNextMatches { + it.serviceAccountId == serviceAccountId + } + .expectComplete() + .verify() + } + @Test fun `it should delete a subject referential`() { val userAccessRights = SubjectReferential( diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt index bb3dd7c06..d066cd8ee 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt @@ -21,6 +21,9 @@ fun extractSubjectOrEmpty(): Mono { fun URI.extractSubjectUuid(): UUID = UUID.fromString(this.toString().substringAfterLast(":")) +fun String.extractSubjectUuid(): UUID = + UUID.fromString(this.substringAfterLast(":")) + fun String.toUUID(): UUID = UUID.fromString(this) diff --git a/shared/src/testFixtures/resources/ngsild/events/authorization/ServiceAccountIdAppendEvent.json b/shared/src/testFixtures/resources/ngsild/events/authorization/ServiceAccountIdAppendEvent.json new file mode 100644 index 000000000..8e731e2b7 --- /dev/null +++ b/shared/src/testFixtures/resources/ngsild/events/authorization/ServiceAccountIdAppendEvent.json @@ -0,0 +1,12 @@ +{ + "operationType": "ATTRIBUTE_APPEND", + "entityId": "urn:ngsi-ld:Client:96e1f1e9-d798-48d7-820e-59f5a9a2abf5", + "entityType": "Client", + "attributeName": "serviceAccountId", + "operationPayload": "{\"type\":\"Property\",\"value\":\"urn:ngsi-ld:User:7cdad168-96ee-4649-b768-a060ac2ef435\"}", + "updatedEntity": "", + "contexts": [ + "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld", + "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" + ] +} From b893174408aa988ac136f40e7f89c21b950e3ec1 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Wed, 22 Dec 2021 15:58:29 +0100 Subject: [PATCH 18/28] feat: add a WithKafkaContainer config / handle groups and stellio-admin access checks --- build.gradle.kts | 2 - entity-service/config/detekt/baseline.xml | 5 +- .../Neo4jAuthorizationRepositoryTest.kt | 3 +- .../repository/EntityRepositoryTests.kt | 3 +- .../entity/repository/Neo4jRepositoryTests.kt | 3 +- .../repository/Neo4jSearchRepositoryTests.kt | 3 +- .../StandaloneNeo4jSearchRepositoryTests.kt | 3 +- .../entity/service/EntityEventServiceTests.kt | 2 +- .../service/SubscriptionEventListenerTests.kt | 2 +- .../service/EntityAccessRightsService.kt | 42 +++++-- .../egm/stellio/search/service/IAMListener.kt | 2 + .../service/SubjectReferentialService.kt | 32 ++++- .../search/web/TemporalEntityHandler.kt | 4 +- .../service/AttributeInstanceServiceTests.kt | 3 +- .../service/EntityAccessRightsServiceTests.kt | 114 ++++++++++++++++-- .../service/EntityEventListenerServiceTest.kt | 2 +- .../search/service/QueryServiceTests.kt | 2 +- .../service/SubjectReferentialServiceTests.kt | 57 +++++++-- .../SubscriptionEventListenerServiceTest.kt | 5 +- .../TemporalEntityAttributeServiceTests.kt | 3 +- .../service/TemporalEntityServiceTests.kt | 2 +- .../search/web/TemporalEntityHandlerTests.kt | 40 +++--- shared/build.gradle.kts | 3 + .../shared/support/WithKafkaContainer.kt | 29 +++++ .../config/detekt/baseline.xml | 2 +- .../EntityEventListenerServiceTests.kt | 2 +- .../service/NotificationServiceTests.kt | 2 +- .../service/SubscriptionEventServiceTests.kt | 2 +- .../service/SubscriptionServiceTests.kt | 3 +- 29 files changed, 302 insertions(+), 75 deletions(-) create mode 100644 shared/src/testFixtures/kotlin/com/egm/stellio/shared/support/WithKafkaContainer.kt diff --git a/build.gradle.kts b/build.gradle.kts index bdbb30269..ec55b8a9d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -78,8 +78,6 @@ subprojects { testImplementation("io.mockk:mockk:1.12.2") testImplementation("io.projectreactor:reactor-test") testImplementation("org.springframework.security:spring-security-test") - testImplementation("org.testcontainers:testcontainers") - testImplementation("org.testcontainers:junit-jupiter") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test") } diff --git a/entity-service/config/detekt/baseline.xml b/entity-service/config/detekt/baseline.xml index 126442f20..17a3b9f42 100644 --- a/entity-service/config/detekt/baseline.xml +++ b/entity-service/config/detekt/baseline.xml @@ -6,8 +6,8 @@ LargeClass:EntityHandlerTests.kt$EntityHandlerTests LargeClass:EntityOperationHandlerTests.kt$EntityOperationHandlerTests LargeClass:EntityServiceTests.kt$EntityServiceTests - LargeClass:Neo4jRepositoryTests.kt$Neo4jRepositoryTests : WithNeo4jContainer - LargeClass:StandaloneNeo4jSearchRepositoryTests.kt$StandaloneNeo4jSearchRepositoryTests : WithNeo4jContainer + LargeClass:Neo4jRepositoryTests.kt$Neo4jRepositoryTests : WithNeo4jContainerWithKafkaContainer + LargeClass:StandaloneNeo4jSearchRepositoryTests.kt$StandaloneNeo4jSearchRepositoryTests : WithNeo4jContainerWithKafkaContainer LongMethod:EntityEventServiceTests.kt$EntityEventServiceTests$@Test fun `it should publish ATTRIBUTE_APPEND and ATTRIBUTE_REPLACE events if attributes were appended and replaced`() LongMethod:EntityHandler.kt$EntityHandler$ @GetMapping(produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun getEntities( @RequestHeader httpHeaders: HttpHeaders, @RequestParam params: MultiValueMap<String, String> ): ResponseEntity<*> LongMethod:EntityServiceTests.kt$EntityServiceTests$@Test fun `it should create a new multi attribute property`() @@ -16,7 +16,6 @@ LongParameterList:EntityEventService.kt$EntityEventService$( entityId: URI, entityType: String, attributeName: String, datasetId: URI? = null, overwrite: Boolean, operationPayload: String, updateOperationResult: UpdateOperationResult, contexts: List<String> ) LongParameterList:EntityService.kt$EntityService$( queryParams: QueryParams, userSub: String, offset: Int, limit: Int, contextLink: String, includeSysAttrs: Boolean ) LongParameterList:EntityService.kt$EntityService$( queryParams: QueryParams, userSub: String, offset: Int, limit: Int, contexts: List<String>, includeSysAttrs: Boolean ) - MaxLineLength:EntityHandlerTests.kt$EntityHandlerTests$ MaxLineLength:Neo4jRepository.kt$Neo4jRepository$ MATCH (a: MaxLineLength:Neo4jRepository.kt$Neo4jRepository$ MATCH (entity: ReturnCount:EntityHandler.kt$EntityHandler$ @GetMapping(produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun getEntities( @RequestHeader httpHeaders: HttpHeaders, @RequestParam params: MultiValueMap<String, String> ): ResponseEntity<*> diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt index 75f89c9e7..89f0a4b89 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt @@ -17,6 +17,7 @@ import com.egm.stellio.entity.repository.EntityRepository import com.egm.stellio.entity.repository.EntitySubjectNode import com.egm.stellio.entity.repository.Neo4jRepository import com.egm.stellio.entity.repository.SubjectNodeInfo +import com.egm.stellio.shared.support.WithKafkaContainer import com.egm.stellio.shared.util.JsonLdUtils.EGM_SPECIFIC_ACCESS_POLICY import com.egm.stellio.shared.util.toUri import org.junit.jupiter.api.AfterEach @@ -29,7 +30,7 @@ import java.net.URI @SpringBootTest @ActiveProfiles("test") -class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer { +class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer { @Autowired private lateinit var neo4jAuthorizationRepository: Neo4jAuthorizationRepository diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/EntityRepositoryTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/EntityRepositoryTests.kt index 575dd6e13..a13ba154b 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/EntityRepositoryTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/EntityRepositoryTests.kt @@ -3,6 +3,7 @@ package com.egm.stellio.entity.repository import com.egm.stellio.entity.config.WithNeo4jContainer import com.egm.stellio.entity.model.Entity import com.egm.stellio.entity.model.Property +import com.egm.stellio.shared.support.WithKafkaContainer import com.egm.stellio.shared.util.toUri import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Test @@ -13,7 +14,7 @@ import java.net.URI @SpringBootTest @ActiveProfiles("test") -class EntityRepositoryTests : WithNeo4jContainer { +class EntityRepositoryTests : WithNeo4jContainer, WithKafkaContainer { @Autowired private lateinit var entityRepository: EntityRepository diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jRepositoryTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jRepositoryTests.kt index fad257d04..90e31bbe5 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jRepositoryTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jRepositoryTests.kt @@ -8,6 +8,7 @@ import com.egm.stellio.entity.model.Relationship import com.egm.stellio.entity.model.toRelationshipTypeName import com.egm.stellio.shared.model.GeoPropertyType import com.egm.stellio.shared.model.NgsiLdGeoPropertyInstance +import com.egm.stellio.shared.support.WithKafkaContainer import com.egm.stellio.shared.util.JsonLdUtils.EGM_OBSERVED_BY import com.egm.stellio.shared.util.toUri import junit.framework.TestCase.assertEquals @@ -28,7 +29,7 @@ import java.net.URI @SpringBootTest @ActiveProfiles("test") -class Neo4jRepositoryTests : WithNeo4jContainer { +class Neo4jRepositoryTests : WithNeo4jContainer, WithKafkaContainer { @Autowired private lateinit var neo4jRepository: Neo4jRepository diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepositoryTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepositoryTests.kt index 748555a99..3e0968db3 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepositoryTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepositoryTests.kt @@ -13,6 +13,7 @@ import com.egm.stellio.entity.model.Entity import com.egm.stellio.entity.model.Property import com.egm.stellio.entity.model.Relationship import com.egm.stellio.shared.model.QueryParams +import com.egm.stellio.shared.support.WithKafkaContainer import com.egm.stellio.shared.util.DEFAULT_CONTEXTS import com.egm.stellio.shared.util.JsonLdUtils import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdKey @@ -33,7 +34,7 @@ import java.net.URI @SpringBootTest @ActiveProfiles("test") @TestPropertySource(properties = ["application.authentication.enabled=true"]) -class Neo4jSearchRepositoryTests : WithNeo4jContainer { +class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { @Autowired private lateinit var searchRepository: SearchRepository diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/StandaloneNeo4jSearchRepositoryTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/StandaloneNeo4jSearchRepositoryTests.kt index 0b27cc302..7732cd0f3 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/StandaloneNeo4jSearchRepositoryTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/StandaloneNeo4jSearchRepositoryTests.kt @@ -5,6 +5,7 @@ import com.egm.stellio.entity.model.Entity import com.egm.stellio.entity.model.Property import com.egm.stellio.entity.model.Relationship import com.egm.stellio.shared.model.QueryParams +import com.egm.stellio.shared.support.WithKafkaContainer import com.egm.stellio.shared.util.DEFAULT_CONTEXTS import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdKey import com.egm.stellio.shared.util.toUri @@ -29,7 +30,7 @@ import java.time.ZonedDateTime @SpringBootTest @ActiveProfiles("test") @TestPropertySource(properties = ["application.authentication.enabled=false"]) -class StandaloneNeo4jSearchRepositoryTests : WithNeo4jContainer { +class StandaloneNeo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { @Autowired private lateinit var searchRepository: SearchRepository diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/service/EntityEventServiceTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/service/EntityEventServiceTests.kt index 62c18300e..e50df3145 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/service/EntityEventServiceTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/service/EntityEventServiceTests.kt @@ -19,7 +19,7 @@ import org.springframework.kafka.core.KafkaTemplate import org.springframework.test.context.ActiveProfiles import org.springframework.util.concurrent.SettableListenableFuture -@SpringBootTest(classes = [EntityEventService::class]) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = [EntityEventService::class]) @ActiveProfiles("test") class EntityEventServiceTests { diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/service/SubscriptionEventListenerTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/service/SubscriptionEventListenerTests.kt index 02b4dab8d..d99477071 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/service/SubscriptionEventListenerTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/service/SubscriptionEventListenerTests.kt @@ -13,7 +13,7 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.ActiveProfiles -@SpringBootTest(classes = [SubscriptionEventListener::class]) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = [SubscriptionEventListener::class]) @ActiveProfiles("test") class SubscriptionEventListenerTests { diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt index 0015ad5ed..a54606b6d 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt @@ -20,6 +20,7 @@ import java.util.UUID class EntityAccessRightsService( private val databaseClient: DatabaseClient, private val r2dbcEntityTemplate: R2dbcEntityTemplate, + private val subjectReferentialService: SubjectReferentialService ) { private val logger = LoggerFactory.getLogger(javaClass) @@ -44,7 +45,7 @@ class EntityAccessRightsService( ) .bind("subject_id", subjectId) .bind("entity_id", entityId) - .bind("access_right", accessRight.toString()) + .bind("access_right", accessRight.attributeName) .fetch() .rowsUpdated() .thenReturn(1) @@ -70,30 +71,51 @@ class EntityAccessRightsService( .thenReturn(1) .onErrorReturn(-1) - fun hasReadRoleOnEntity(subjectId: UUID, entityId: URI): Mono = - hasRoleOnEntity(subjectId, entityId, listOf(R_CAN_READ, R_CAN_WRITE, R_CAN_ADMIN)) + fun canReadEntity(subjectId: UUID, entityId: URI): Mono = + checkHasAccessRight(subjectId, entityId, listOf(R_CAN_READ, R_CAN_WRITE, R_CAN_ADMIN)) - fun hasWriteRoleOnEntity(subjectId: UUID, entityId: URI): Mono = - hasRoleOnEntity(subjectId, entityId, listOf(R_CAN_WRITE, R_CAN_ADMIN)) + fun canWriteEntity(subjectId: UUID, entityId: URI): Mono = + checkHasAccessRight(subjectId, entityId, listOf(R_CAN_WRITE, R_CAN_ADMIN)) - fun hasRoleOnEntity(subjectId: UUID, entityId: URI, accessRights: List): Mono = + private fun checkHasAccessRight(subjectId: UUID, entityId: URI, accessRights: List): Mono = + subjectReferentialService.hasStellioAdminRole(subjectId) + .flatMap { + // if user has stellio-admin role, no need to check further + if (it) Mono.just(true) + else { + // create a list with user id and its groups memberships ... + subjectReferentialService.retrieve(subjectId) + .map { subjectReferential -> + subjectReferential.groupsMemberships ?: emptyList() + } + .map { groups -> + groups.plus(subjectId) + } + .flatMap { uuids -> + // ... and check if it has the required role with at least one of them + hasRoleOnEntity(uuids, entityId, accessRights) + } + } + } + + private fun hasRoleOnEntity(uuids: List, entityId: URI, accessRights: List): Mono = databaseClient .sql( """ SELECT COUNT(subject_id) as count FROM entity_access_rights - WHERE subject_id = :subject_id + WHERE subject_id IN(:uuids) AND entity_id = :entity_id AND access_right IN(:access_rights) """ ) - .bind("subject_id", subjectId) + .bind("uuids", uuids) .bind("entity_id", entityId) - .bind("access_rights", accessRights.map { it.toString() }) + .bind("access_rights", accessRights.map { it.attributeName }) .fetch() .one() .map { - it["count"] as Long == 1L + it["count"] as Long >= 1L } .onErrorResume { logger.error("Error while checking role on entity: $it") diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt index 5c35991f4..e01a2a3ff 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt @@ -91,6 +91,8 @@ class IAMListener( subjectUuid, groupId.extractSubjectUuid() ) + } else { + logger.info("Received unknown attribute name: ${attributeAppendEvent.attributeName}") } } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt index 50ea6ba38..fafa5e09d 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt @@ -35,7 +35,7 @@ class SubjectReferentialService( ) .bind("subject_id", subjectReferential.subjectId) .bind("subject_type", subjectReferential.subjectType.toString()) - .bind("global_roles", subjectReferential.globalRoles?.map { it.toString() }?.toTypedArray()) + .bind("global_roles", subjectReferential.globalRoles?.map { it.key }?.toTypedArray()) .bind("groups_memberships", subjectReferential.groupsMemberships?.toTypedArray()) .fetch() .rowsUpdated() @@ -57,7 +57,29 @@ class SubjectReferentialService( .bind("subject_id", subjectId) .fetch() .one() - .map { rowToUserAccessRights(it) } + .map { rowToSubjectReferential(it) } + + fun hasStellioAdminRole(subjectId: UUID): Mono = + databaseClient + .sql( + """ + SELECT COUNT(subject_id) as count + FROM subject_referential + WHERE subject_id = :subject_id + AND '${GlobalRole.STELLIO_ADMIN.key}' = ANY(global_roles) + """ + ) + .bind("subject_id", subjectId) + .fetch() + .one() + .log() + .map { + it["count"] as Long == 1L + } + .onErrorResume { + logger.error("Error while checking stellio-admin role for user: $it") + Mono.just(false) + } @Transactional fun setGlobalRoles(subjectId: UUID, newRoles: List): Mono = @@ -70,7 +92,7 @@ class SubjectReferentialService( """ ) .bind("subject_id", subjectId) - .bind("global_roles", newRoles.map { it.toString() }.toTypedArray()) + .bind("global_roles", newRoles.map { it.key }.toTypedArray()) .fetch() .rowsUpdated() .thenReturn(1) @@ -161,12 +183,12 @@ class SubjectReferentialService( .matching(Query.query(Criteria.where("subject_id").`is`(subjectId))) .all() - private fun rowToUserAccessRights(row: Map) = + private fun rowToSubjectReferential(row: Map) = SubjectReferential( subjectId = row["subject_id"] as UUID, subjectType = SubjectType.valueOf(row["subject_type"] as String), serviceAccountId = row["service_account_id"] as UUID?, - globalRoles = (row["global_roles"] as Array?)?.map { GlobalRole.valueOf(it) }, + globalRoles = (row["global_roles"] as Array?)?.map { GlobalRole.forKey(it) }, groupsMemberships = (row["groups_memberships"] as Array?)?.map { it.toUUID() } ) } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt index 47592c8b2..a14cd4a5b 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt @@ -60,7 +60,7 @@ class TemporalEntityHandler( ): ResponseEntity<*> { val userId = extractSubjectOrEmpty().awaitFirst() val canWriteEntity = - entityAccessRightsService.hasWriteRoleOnEntity(UUID.fromString(userId), entityId.toUri()).awaitFirst() + entityAccessRightsService.canWriteEntity(UUID.fromString(userId), entityId.toUri()).awaitFirst() if (!canWriteEntity) throw AccessDeniedException("User forbidden write access to entity $entityId") @@ -145,7 +145,7 @@ class TemporalEntityHandler( val userId = extractSubjectOrEmpty().awaitFirst() val canReadEntity = - entityAccessRightsService.hasReadRoleOnEntity(UUID.fromString(userId), entityId.toUri()).awaitFirst() + entityAccessRightsService.canReadEntity(UUID.fromString(userId), entityId.toUri()).awaitFirst() if (!canReadEntity) throw AccessDeniedException("User forbidden read access to entity $entityId") diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/AttributeInstanceServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/AttributeInstanceServiceTests.kt index f687551cc..40d542c3d 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/AttributeInstanceServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/AttributeInstanceServiceTests.kt @@ -7,6 +7,7 @@ import com.egm.stellio.search.model.TemporalEntityAttribute import com.egm.stellio.search.model.TemporalQuery import com.egm.stellio.search.support.WithTimescaleContainer import com.egm.stellio.shared.model.BadRequestDataException +import com.egm.stellio.shared.support.WithKafkaContainer import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_VALUE_KW import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CORE_CONTEXT @@ -37,7 +38,7 @@ import kotlin.random.Random @SpringBootTest @ActiveProfiles("test") -class AttributeInstanceServiceTests : WithTimescaleContainer { +class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer { @Autowired private lateinit var attributeInstanceService: AttributeInstanceService diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityAccessRightsServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityAccessRightsServiceTests.kt index fe56e81ae..d4997bfea 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityAccessRightsServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityAccessRightsServiceTests.kt @@ -1,20 +1,30 @@ package com.egm.stellio.search.service import com.egm.stellio.search.model.EntityAccessRights +import com.egm.stellio.search.model.SubjectReferential import com.egm.stellio.search.support.WithTimescaleContainer +import com.egm.stellio.shared.support.WithKafkaContainer +import com.egm.stellio.shared.util.SubjectType import com.egm.stellio.shared.util.toUri +import com.ninjasquad.springmockk.MockkBean +import io.mockk.Called +import io.mockk.every +import io.mockk.mockkClass +import io.mockk.verify import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.data.r2dbc.core.R2dbcEntityTemplate import org.springframework.test.context.ActiveProfiles +import reactor.core.publisher.Mono import reactor.test.StepVerifier import java.util.UUID @SpringBootTest @ActiveProfiles("test") -class EntityAccessRightsServiceTests : WithTimescaleContainer { +class EntityAccessRightsServiceTests : WithTimescaleContainer, WithKafkaContainer { @Autowired private lateinit var entityAccessRightsService: EntityAccessRightsService @@ -22,8 +32,20 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer { @Autowired private lateinit var r2dbcEntityTemplate: R2dbcEntityTemplate + @MockkBean(relaxed = true) + private lateinit var subjectReferentialService: SubjectReferentialService + private val subjectUuid = UUID.fromString("0768A6D5-D87B-4209-9A22-8C40A8961A79") + private val groupUuid = UUID.fromString("220FC854-3609-404B-BC77-F2DFE332B27B") private val entityId = "urn:ngsi-ld:Entity:1111".toUri() + private val defaultMockSubjectReferential = mockkClass(SubjectReferential::class) + + @BeforeEach + fun setDefaultBehaviorOnSubjectReferential() { + every { subjectReferentialService.hasStellioAdminRole(subjectUuid) } answers { Mono.just(false) } + every { subjectReferentialService.retrieve(subjectUuid) } answers { Mono.just(defaultMockSubjectReferential) } + every { defaultMockSubjectReferential.groupsMemberships } returns emptyList() + } @AfterEach fun clearEntityAccessRightsTable() { @@ -41,7 +63,7 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer { .verify() StepVerifier - .create(entityAccessRightsService.hasReadRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:1111".toUri())) + .create(entityAccessRightsService.canReadEntity(subjectUuid, "urn:ngsi-ld:Entity:1111".toUri())) .expectNextMatches { it == true } .expectComplete() .verify() @@ -62,14 +84,14 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer { .verify() StepVerifier - .create(entityAccessRightsService.hasReadRoleOnEntity(subjectUuid, entityId)) + .create(entityAccessRightsService.canReadEntity(subjectUuid, entityId)) .expectNextMatches { it == false } .expectComplete() .verify() } @Test - fun `it should find if an user has a read role on a entity`() { + fun `it should allow an user having a direct read role on a entity`() { StepVerifier .create(entityAccessRightsService.setReadRoleOnEntity(subjectUuid, entityId)) .expectNextMatches { it == 1 } @@ -83,21 +105,99 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer { .verify() StepVerifier - .create(entityAccessRightsService.hasReadRoleOnEntity(subjectUuid, entityId)) + .create(entityAccessRightsService.canReadEntity(subjectUuid, entityId)) .expectNextMatches { it == true } .expectComplete() .verify() StepVerifier - .create(entityAccessRightsService.hasReadRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:2222".toUri())) + .create(entityAccessRightsService.canReadEntity(subjectUuid, "urn:ngsi-ld:Entity:2222".toUri())) .expectNextMatches { it == false } .expectComplete() .verify() StepVerifier - .create(entityAccessRightsService.hasReadRoleOnEntity(subjectUuid, "urn:ngsi-ld:Entity:6666".toUri())) + .create(entityAccessRightsService.canWriteEntity(subjectUuid, "urn:ngsi-ld:Entity:6666".toUri())) + .expectNextMatches { it == true } + .expectComplete() + .verify() + } + + @Test + fun `it should allow an user having a read role on a entity via a group membership`() { + every { + subjectReferentialService.retrieve(subjectUuid) + } answers { + Mono.just( + SubjectReferential( + subjectId = subjectUuid, + subjectType = SubjectType.USER, + groupsMemberships = listOf(groupUuid, UUID.randomUUID()) + ) + ) + } + + entityAccessRightsService.setReadRoleOnEntity(groupUuid, entityId).block() + + StepVerifier + .create(entityAccessRightsService.canReadEntity(subjectUuid, entityId)) .expectNextMatches { it == true } .expectComplete() .verify() + + StepVerifier + .create(entityAccessRightsService.canReadEntity(subjectUuid, "urn:ngsi-ld:Entity:2222".toUri())) + .expectNextMatches { it == false } + .expectComplete() + .verify() + } + + @Test + fun `it should allow an user having a read role on a entity both directly and via a group membership`() { + every { + subjectReferentialService.retrieve(subjectUuid) + } answers { + Mono.just( + SubjectReferential( + subjectId = subjectUuid, + subjectType = SubjectType.USER, + groupsMemberships = listOf(groupUuid, UUID.randomUUID()) + ) + ) + } + + entityAccessRightsService.setReadRoleOnEntity(groupUuid, entityId).block() + entityAccessRightsService.setReadRoleOnEntity(subjectUuid, entityId).block() + + StepVerifier + .create(entityAccessRightsService.canReadEntity(subjectUuid, entityId)) + .expectNextMatches { it == true } + .expectComplete() + .verify() + + StepVerifier + .create(entityAccessRightsService.canReadEntity(subjectUuid, "urn:ngsi-ld:Entity:2222".toUri())) + .expectNextMatches { it == false } + .expectComplete() + .verify() + } + + @Test + fun `it should allow an user having the stellio-admin role to read any entity`() { + every { subjectReferentialService.hasStellioAdminRole(subjectUuid) } answers { Mono.just(true) } + + StepVerifier + .create(entityAccessRightsService.canReadEntity(subjectUuid, entityId)) + .expectNextMatches { it == true } + .expectComplete() + .verify() + + StepVerifier + .create(entityAccessRightsService.canReadEntity(subjectUuid, "urn:ngsi-ld:Entity:2222".toUri())) + .expectNextMatches { it == true } + .expectComplete() + .verify() + + verify { subjectReferentialService.retrieve(eq(subjectUuid)) wasNot Called } } } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityEventListenerServiceTest.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityEventListenerServiceTest.kt index 5e0b11a69..77d862e10 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityEventListenerServiceTest.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityEventListenerServiceTest.kt @@ -23,7 +23,7 @@ import reactor.core.publisher.Mono import java.time.ZonedDateTime import java.util.UUID -@SpringBootTest(classes = [EntityEventListenerService::class]) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = [EntityEventListenerService::class]) @ActiveProfiles("test") class EntityEventListenerServiceTest { diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/QueryServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/QueryServiceTests.kt index 132082514..9948bff41 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/QueryServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/QueryServiceTests.kt @@ -29,7 +29,7 @@ import reactor.core.publisher.Mono import reactor.kotlin.core.publisher.toFlux import java.time.ZonedDateTime -@SpringBootTest(classes = [QueryService::class]) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = [QueryService::class]) @ActiveProfiles("test") @ExperimentalCoroutinesApi class QueryServiceTests { diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectReferentialServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectReferentialServiceTests.kt index 11481a8e5..a07965bc6 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectReferentialServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectReferentialServiceTests.kt @@ -2,7 +2,9 @@ package com.egm.stellio.search.service import com.egm.stellio.search.model.SubjectReferential import com.egm.stellio.search.support.WithTimescaleContainer -import com.egm.stellio.shared.util.GlobalRole +import com.egm.stellio.shared.support.WithKafkaContainer +import com.egm.stellio.shared.util.GlobalRole.STELLIO_ADMIN +import com.egm.stellio.shared.util.GlobalRole.STELLIO_CREATOR import com.egm.stellio.shared.util.SubjectType import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test @@ -15,7 +17,7 @@ import java.util.UUID @SpringBootTest @ActiveProfiles("test") -class SubjectReferentialServiceTests : WithTimescaleContainer { +class SubjectReferentialServiceTests : WithTimescaleContainer, WithKafkaContainer { @Autowired private lateinit var subjectReferentialService: SubjectReferentialService @@ -38,7 +40,7 @@ class SubjectReferentialServiceTests : WithTimescaleContainer { val subjectReferential = SubjectReferential( subjectId = subjectUuid, subjectType = SubjectType.USER, - globalRoles = listOf(GlobalRole.STELLIO_ADMIN) + globalRoles = listOf(STELLIO_ADMIN) ) StepVerifier @@ -53,7 +55,7 @@ class SubjectReferentialServiceTests : WithTimescaleContainer { val subjectReferential = SubjectReferential( subjectId = subjectUuid, subjectType = SubjectType.USER, - globalRoles = listOf(GlobalRole.STELLIO_ADMIN) + globalRoles = listOf(STELLIO_ADMIN) ) subjectReferentialService.create(subjectReferential).block() @@ -63,7 +65,7 @@ class SubjectReferentialServiceTests : WithTimescaleContainer { .expectNextMatches { it.subjectId == subjectUuid && it.subjectType == SubjectType.USER && - it.globalRoles == listOf(GlobalRole.STELLIO_ADMIN) + it.globalRoles == listOf(STELLIO_ADMIN) } .expectComplete() .verify() @@ -79,7 +81,7 @@ class SubjectReferentialServiceTests : WithTimescaleContainer { subjectReferentialService.create(subjectReferential).block() StepVerifier - .create(subjectReferentialService.setGlobalRoles(subjectUuid, listOf(GlobalRole.STELLIO_ADMIN))) + .create(subjectReferentialService.setGlobalRoles(subjectUuid, listOf(STELLIO_ADMIN))) .expectNextMatches { it == 1 } .expectComplete() .verify() @@ -87,7 +89,7 @@ class SubjectReferentialServiceTests : WithTimescaleContainer { StepVerifier .create(subjectReferentialService.retrieve(subjectUuid)) .expectNextMatches { - it.globalRoles == listOf(GlobalRole.STELLIO_ADMIN) + it.globalRoles == listOf(STELLIO_ADMIN) } .expectComplete() .verify() @@ -107,6 +109,45 @@ class SubjectReferentialServiceTests : WithTimescaleContainer { .verify() } + @Test + fun `it should find if an user is a stellio admin or not`() { + val subjectReferential = SubjectReferential( + subjectId = subjectUuid, + subjectType = SubjectType.USER, + globalRoles = listOf(STELLIO_ADMIN) + ) + + subjectReferentialService.create(subjectReferential).block() + + StepVerifier + .create(subjectReferentialService.hasStellioAdminRole(subjectUuid)) + .expectNextMatches { + it + } + .expectComplete() + .verify() + + subjectReferentialService.resetGlobalRoles(subjectUuid).block() + + StepVerifier + .create(subjectReferentialService.hasStellioAdminRole(subjectUuid)) + .expectNextMatches { + !it + } + .expectComplete() + .verify() + + subjectReferentialService.setGlobalRoles(subjectUuid, listOf(STELLIO_ADMIN, STELLIO_CREATOR)).block() + + StepVerifier + .create(subjectReferentialService.hasStellioAdminRole(subjectUuid)) + .expectNextMatches { + it + } + .expectComplete() + .verify() + } + @Test fun `it should add a group membership to an user`() { val userAccessRights = SubjectReferential( @@ -212,7 +253,7 @@ class SubjectReferentialServiceTests : WithTimescaleContainer { val userAccessRights = SubjectReferential( subjectId = subjectUuid, subjectType = SubjectType.USER, - globalRoles = listOf(GlobalRole.STELLIO_ADMIN) + globalRoles = listOf(STELLIO_ADMIN) ) subjectReferentialService.create(userAccessRights).block() diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubscriptionEventListenerServiceTest.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubscriptionEventListenerServiceTest.kt index 46d0a3224..9aec38b25 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubscriptionEventListenerServiceTest.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubscriptionEventListenerServiceTest.kt @@ -15,7 +15,10 @@ import org.springframework.test.context.ActiveProfiles import reactor.core.publisher.Mono import java.util.UUID -@SpringBootTest(classes = [SubscriptionEventListenerService::class]) +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.NONE, + classes = [SubscriptionEventListenerService::class] +) @ActiveProfiles("test") class SubscriptionEventListenerServiceTest { diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeServiceTests.kt index 262633a84..34e4e85d5 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeServiceTests.kt @@ -4,6 +4,7 @@ import com.egm.stellio.search.model.AttributeInstance import com.egm.stellio.search.model.EntityPayload import com.egm.stellio.search.model.TemporalEntityAttribute import com.egm.stellio.search.support.WithTimescaleContainer +import com.egm.stellio.shared.support.WithKafkaContainer import com.egm.stellio.shared.util.APIC_COMPOUND_CONTEXT import com.egm.stellio.shared.util.JsonUtils.deserializeObject import com.egm.stellio.shared.util.JsonUtils.serializeObject @@ -27,7 +28,7 @@ import java.time.ZonedDateTime @SpringBootTest @ActiveProfiles("test") -class TemporalEntityAttributeServiceTests : WithTimescaleContainer { +class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaContainer { @Autowired @SpykBean diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityServiceTests.kt index 887f7976b..c694fd949 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityServiceTests.kt @@ -21,7 +21,7 @@ import java.time.ZoneOffset import java.time.ZonedDateTime import java.util.UUID -@SpringBootTest(classes = [TemporalEntityService::class]) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = [TemporalEntityService::class]) @ActiveProfiles("test") class TemporalEntityServiceTests { diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt index 99a96cbb9..7d080b802 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt @@ -92,7 +92,7 @@ class TemporalEntityHandlerTests { loadSampleData("fragments/temporal_entity_fragment_one_attribute_one_instance.jsonld") val temporalEntityAttributeUuid = UUID.randomUUID() - every { entityAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.canWriteEntity(any(), any()) } answers { Mono.just(true) } every { temporalEntityAttributeService.getForEntityAndAttribute(any(), any()) } answers { Mono.just(temporalEntityAttributeUuid) } @@ -134,7 +134,7 @@ class TemporalEntityHandlerTests { loadSampleData("fragments/temporal_entity_fragment_one_attribute_many_instances.jsonld") val temporalEntityAttributeUuid = UUID.randomUUID() - every { entityAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.canWriteEntity(any(), any()) } answers { Mono.just(true) } every { temporalEntityAttributeService.getForEntityAndAttribute(any(), any()) } answers { Mono.just(temporalEntityAttributeUuid) } @@ -176,7 +176,7 @@ class TemporalEntityHandlerTests { loadSampleData("fragments/temporal_entity_fragment_many_attributes_one_instance.jsonld") val temporalEntityAttributeUuid = UUID.randomUUID() - every { entityAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.canWriteEntity(any(), any()) } answers { Mono.just(true) } every { temporalEntityAttributeService.getForEntityAndAttribute(any(), any()) } answers { Mono.just(temporalEntityAttributeUuid) } @@ -221,7 +221,7 @@ class TemporalEntityHandlerTests { loadSampleData("fragments/temporal_entity_fragment_many_attributes_many_instances.jsonld") val temporalEntityAttributeUuid = UUID.randomUUID() - every { entityAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.canWriteEntity(any(), any()) } answers { Mono.just(true) } every { temporalEntityAttributeService.getForEntityAndAttribute(any(), any()) } answers { Mono.just(temporalEntityAttributeUuid) } @@ -262,7 +262,7 @@ class TemporalEntityHandlerTests { @Test fun `it should return a 400 if temporal entity fragment is badly formed`() { - every { entityAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.canWriteEntity(any(), any()) } answers { Mono.just(true) } webClient.post() .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs") @@ -284,7 +284,7 @@ class TemporalEntityHandlerTests { @Test fun `it should return a 403 is user is not authorized to write on the entity`() { - every { entityAccessRightsService.hasWriteRoleOnEntity(any(), any()) } answers { Mono.just(false) } + every { entityAccessRightsService.canWriteEntity(any(), any()) } answers { Mono.just(false) } webClient.post() .uri("/ngsi-ld/v1/temporal/entities/$entityUri/attrs") @@ -302,7 +302,7 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if timerel is present without time query param`() { - every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=before") @@ -321,7 +321,7 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if time is present without timerel query param`() { - every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?time=2020-10-29T18:00:00Z") @@ -341,7 +341,7 @@ class TemporalEntityHandlerTests { @Test fun `it should give a 200 if no timerel and no time query params are in the request`() { coEvery { queryService.queryTemporalEntity(any(), any(), any(), any()) } returns emptyMap() - every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri("/ngsi-ld/v1/temporal/entities/$entityUri") @@ -351,7 +351,7 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if timerel is between and no endTime provided`() { - every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=between&time=startTime") @@ -370,7 +370,7 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if time is not parsable`() { - every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=before&time=badTime") @@ -389,7 +389,7 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if timerel is not a valid value`() { - every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=befor&time=badTime") @@ -408,7 +408,7 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if timerel is between and endTime is not parseable`() { - every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri( @@ -430,7 +430,7 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if one of time bucket or aggregate is missing`() { - every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri( @@ -452,7 +452,7 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if aggregate function is unknown`() { - every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri( @@ -474,7 +474,7 @@ class TemporalEntityHandlerTests { @Test fun `it should return a 404 if temporal entity attribute does not exist`() { - every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } coEvery { queryService.queryTemporalEntity(any(), any(), any(), any()) } throws ResourceNotFoundException("Entity urn:ngsi-ld:BeeHive:TESTC was not found") @@ -499,7 +499,7 @@ class TemporalEntityHandlerTests { @Test fun `it should return a 200 if minimal required parameters are valid`() { - every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } coEvery { queryService.queryTemporalEntity(any(), any(), any(), any()) } returns emptyMap() webClient.get() @@ -529,7 +529,7 @@ class TemporalEntityHandlerTests { @Test fun `it should return an entity with two temporal properties evolution`() { mockWithIncomingAndOutgoingTemporalProperties(false) - every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri( @@ -550,7 +550,7 @@ class TemporalEntityHandlerTests { @Test fun `it should return a json entity with two temporal properties evolution`() { mockWithIncomingAndOutgoingTemporalProperties(false) - every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri( @@ -573,7 +573,7 @@ class TemporalEntityHandlerTests { @Test fun `it should return an entity with two temporal properties evolution with temporalValues option`() { mockWithIncomingAndOutgoingTemporalProperties(true) - every { entityAccessRightsService.hasReadRoleOnEntity(any(), any()) } answers { Mono.just(true) } + every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } webClient.get() .uri( diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 29cec4691..236728b2c 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -26,5 +26,8 @@ dependencies { // to ensure we are using mocks and spies from springmockk lib instead exclude(module = "mockito-core") } + testFixturesApi("org.testcontainers:testcontainers") + testFixturesApi("org.testcontainers:junit-jupiter") + testFixturesApi("org.testcontainers:kafka") testImplementation("org.hamcrest:hamcrest:2.2") } diff --git a/shared/src/testFixtures/kotlin/com/egm/stellio/shared/support/WithKafkaContainer.kt b/shared/src/testFixtures/kotlin/com/egm/stellio/shared/support/WithKafkaContainer.kt new file mode 100644 index 000000000..dd6b75473 --- /dev/null +++ b/shared/src/testFixtures/kotlin/com/egm/stellio/shared/support/WithKafkaContainer.kt @@ -0,0 +1,29 @@ +package com.egm.stellio.shared.support + +import org.springframework.test.context.DynamicPropertyRegistry +import org.springframework.test.context.DynamicPropertySource +import org.testcontainers.containers.KafkaContainer +import org.testcontainers.junit.jupiter.Container +import org.testcontainers.junit.jupiter.Testcontainers +import org.testcontainers.utility.DockerImageName + +@Testcontainers +interface WithKafkaContainer { + companion object { + + private val kafkaImage: DockerImageName = DockerImageName.parse("confluentinc/cp-kafka:5.4.1") + + @Container + val kafkaContainer = KafkaContainer(kafkaImage) + + @JvmStatic + @DynamicPropertySource + fun properties(registry: DynamicPropertyRegistry) { + registry.add("spring.kafka.bootstrap-servers") { kafkaContainer.bootstrapServers } + } + + init { + kafkaContainer.start() + } + } +} diff --git a/subscription-service/config/detekt/baseline.xml b/subscription-service/config/detekt/baseline.xml index a2141236a..67f36f91d 100644 --- a/subscription-service/config/detekt/baseline.xml +++ b/subscription-service/config/detekt/baseline.xml @@ -2,7 +2,7 @@ - LargeClass:SubscriptionServiceTests.kt$SubscriptionServiceTests : WithTimescaleContainer + LargeClass:SubscriptionServiceTests.kt$SubscriptionServiceTests : WithTimescaleContainerWithKafkaContainer LongParameterList:FixtureUtils.kt$( withQueryAndGeoQuery: Pair<Boolean, Boolean> = Pair(true, true), withEndpointInfo: Boolean = true, withNotifParams: Pair<FormatType, List<String>> = Pair(FormatType.NORMALIZED, emptyList()), withModifiedAt: Boolean = false, georel: String = "within", coordinates: Any = "[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]" ) MaxLineLength:SubscriptionService.kt$SubscriptionService$ AND ( string_to_array(watched_attributes, ',') && string_to_array(:updatedAttributes, ',') OR watched_attributes IS NULL ) ReturnCount:QueryUtils.kt$QueryUtils$fun extractGeorelParams(georel: String): Triple<String, String?, String?> diff --git a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/EntityEventListenerServiceTests.kt b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/EntityEventListenerServiceTests.kt index 0f45d630d..e2eee5dda 100644 --- a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/EntityEventListenerServiceTests.kt +++ b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/EntityEventListenerServiceTests.kt @@ -14,7 +14,7 @@ import org.springframework.core.io.ClassPathResource import org.springframework.test.context.ActiveProfiles import reactor.core.publisher.Mono -@SpringBootTest(classes = [EntityEventListenerService::class]) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = [EntityEventListenerService::class]) @ActiveProfiles("test") class EntityEventListenerServiceTests { diff --git a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/NotificationServiceTests.kt b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/NotificationServiceTests.kt index 31d155347..8521d01b8 100644 --- a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/NotificationServiceTests.kt +++ b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/NotificationServiceTests.kt @@ -40,7 +40,7 @@ import reactor.core.publisher.Flux import reactor.core.publisher.Mono import reactor.test.StepVerifier -@SpringBootTest(classes = [NotificationService::class]) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = [NotificationService::class]) @ActiveProfiles("test") class NotificationServiceTests { diff --git a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/SubscriptionEventServiceTests.kt b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/SubscriptionEventServiceTests.kt index b77e2bf0d..e76b5d20c 100644 --- a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/SubscriptionEventServiceTests.kt +++ b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/SubscriptionEventServiceTests.kt @@ -20,7 +20,7 @@ import reactor.core.publisher.Mono import java.time.Instant import java.time.ZoneOffset -@SpringBootTest(classes = [SubscriptionEventService::class]) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = [SubscriptionEventService::class]) @ActiveProfiles("test") class SubscriptionEventServiceTests { diff --git a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/SubscriptionServiceTests.kt b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/SubscriptionServiceTests.kt index 10efc59a6..b9f83734a 100644 --- a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/SubscriptionServiceTests.kt +++ b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/SubscriptionServiceTests.kt @@ -3,6 +3,7 @@ package com.egm.stellio.subscription.service import com.egm.stellio.shared.model.BadRequestDataException import com.egm.stellio.shared.model.NotImplementedException import com.egm.stellio.shared.model.Notification +import com.egm.stellio.shared.support.WithKafkaContainer import com.egm.stellio.shared.util.APIC_COMPOUND_CONTEXT import com.egm.stellio.shared.util.matchContent import com.egm.stellio.shared.util.toUri @@ -28,7 +29,7 @@ import java.time.ZonedDateTime @SpringBootTest @ActiveProfiles("test") -class SubscriptionServiceTests : WithTimescaleContainer { +class SubscriptionServiceTests : WithTimescaleContainer, WithKafkaContainer { @Autowired private lateinit var subscriptionService: SubscriptionService From c11a4107aec7b5cc59317b18310abdccde49794f Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Thu, 23 Dec 2021 14:33:05 +0100 Subject: [PATCH 19/28] fix(search): add missing checks for clients - for clients with a service account, we have to check on this value for roles and memberships --- .../search/service/EntityAccessRightsService.kt | 6 +++--- .../search/service/SubjectReferentialService.kt | 17 ++++++++--------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt index a54606b6d..978e2943b 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt @@ -41,7 +41,7 @@ class EntityAccessRightsService( VALUES (:subject_id, :entity_id, :access_right) ON CONFLICT (subject_id, entity_id, access_right) DO UPDATE SET access_right = :access_right - """ + """.trimIndent() ) .bind("subject_id", subjectId) .bind("entity_id", entityId) @@ -62,7 +62,7 @@ class EntityAccessRightsService( DELETE from entity_access_rights WHERE subject_id = :subject_id AND entity_id = :entity_id - """ + """.trimIndent() ) .bind("subject_id", subjectId) .bind("entity_id", entityId) @@ -107,7 +107,7 @@ class EntityAccessRightsService( WHERE subject_id IN(:uuids) AND entity_id = :entity_id AND access_right IN(:access_rights) - """ + """.trimIndent() ) .bind("uuids", uuids) .bind("entity_id", entityId) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt index fafa5e09d..b1652291e 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt @@ -31,7 +31,7 @@ class SubjectReferentialService( INSERT INTO subject_referential (subject_id, subject_type, global_roles, groups_memberships) VALUES (:subject_id, :subject_type, :global_roles, :groups_memberships) - """ + """.trimIndent() ) .bind("subject_id", subjectReferential.subjectId) .bind("subject_type", subjectReferential.subjectType.toString()) @@ -52,7 +52,7 @@ class SubjectReferentialService( SELECT * FROM subject_referential WHERE subject_id = :subject_id - """ + """.trimIndent() ) .bind("subject_id", subjectId) .fetch() @@ -65,14 +65,13 @@ class SubjectReferentialService( """ SELECT COUNT(subject_id) as count FROM subject_referential - WHERE subject_id = :subject_id + WHERE (subject_id = :subject_id OR service_account_id = :subject_id) AND '${GlobalRole.STELLIO_ADMIN.key}' = ANY(global_roles) - """ + """.trimIndent() ) .bind("subject_id", subjectId) .fetch() .one() - .log() .map { it["count"] as Long == 1L } @@ -89,7 +88,7 @@ class SubjectReferentialService( UPDATE subject_referential SET global_roles = :global_roles WHERE subject_id = :subject_id - """ + """.trimIndent() ) .bind("subject_id", subjectId) .bind("global_roles", newRoles.map { it.key }.toTypedArray()) @@ -109,7 +108,7 @@ class SubjectReferentialService( UPDATE subject_referential SET global_roles = null WHERE subject_id = :subject_id - """ + """.trimIndent() ) .bind("subject_id", subjectId) .fetch() @@ -127,7 +126,7 @@ class SubjectReferentialService( """ UPDATE subject_referential SET groups_memberships = array_append(groups_memberships, :group_id::text) - WHERE subject_id = :subject_id + WHERE (subject_id = :subject_id OR service_account_id = :subject_id) """.trimIndent() ) .bind("subject_id", subjectId) @@ -146,7 +145,7 @@ class SubjectReferentialService( """ UPDATE subject_referential SET groups_memberships = array_remove(groups_memberships, :group_id::text) - WHERE subject_id = :subject_id + WHERE (subject_id = :subject_id OR service_account_id = :subject_id) """.trimIndent() ) .bind("subject_id", subjectId) From 0d2abce9a5c08bb1c9c3c46e6e2ec4aef752a2cc Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Thu, 23 Dec 2021 14:53:20 +0100 Subject: [PATCH 20/28] feat(search): add support for default role at subject creation time --- .../egm/stellio/search/service/IAMListener.kt | 10 +++++++++- .../stellio/search/service/IAMListenerTests.kt | 18 ++++++++++++++++++ .../UserCreateEventDefaultRole.json | 10 ++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 shared/src/testFixtures/resources/ngsild/events/authorization/UserCreateEventDefaultRole.json diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt index e01a2a3ff..392ef23e6 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt @@ -14,6 +14,7 @@ import com.egm.stellio.shared.util.SubjectType import com.egm.stellio.shared.util.extractSubjectUuid import com.egm.stellio.shared.util.toUri import com.fasterxml.jackson.databind.node.ArrayNode +import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.slf4j.LoggerFactory import org.springframework.kafka.annotation.KafkaListener @@ -49,9 +50,16 @@ class IAMListener( } private fun createSubjectReferential(entityCreateEvent: EntityCreateEvent) { + val operationPayloadNode = jacksonObjectMapper().readTree(entityCreateEvent.operationPayload) + val defaultRole = + if (operationPayloadNode.has("roles")) { + val roleAsText = (operationPayloadNode["roles"] as ObjectNode)["value"].asText() + listOf(GlobalRole.forKey(roleAsText)) + } else null val subjectReferential = SubjectReferential( subjectId = entityCreateEvent.entityId.extractSubjectUuid(), - subjectType = SubjectType.valueOf(entityCreateEvent.entityType.uppercase()) + subjectType = SubjectType.valueOf(entityCreateEvent.entityType.uppercase()), + globalRoles = defaultRole ) subjectReferentialService.create(subjectReferential) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt index 57b1e4ad2..6d5efcc96 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt @@ -45,6 +45,24 @@ class IAMListenerTests { confirmVerified() } + @Test + fun `it should handle a create event for a subject with a default role`() { + val subjectCreateEvent = loadSampleData("events/authorization/UserCreateEventDefaultRole.json") + + iamListener.processMessage(subjectCreateEvent) + + verify { + subjectReferentialService.create( + match { + it.subjectId == "6ad19fe0-fc11-4024-85f2-931c6fa6f7e0".toUUID() && + it.subjectType == SubjectType.USER && + it.globalRoles == listOf(GlobalRole.STELLIO_CREATOR) + } + ) + } + confirmVerified() + } + @Test fun `it should handle a delete event for a subject`() { val subjectDeleteEvent = loadSampleData("events/authorization/UserDeleteEvent.json") diff --git a/shared/src/testFixtures/resources/ngsild/events/authorization/UserCreateEventDefaultRole.json b/shared/src/testFixtures/resources/ngsild/events/authorization/UserCreateEventDefaultRole.json new file mode 100644 index 000000000..a144c86aa --- /dev/null +++ b/shared/src/testFixtures/resources/ngsild/events/authorization/UserCreateEventDefaultRole.json @@ -0,0 +1,10 @@ +{ + "operationType": "ENTITY_CREATE", + "entityId": "urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0", + "entityType": "User", + "operationPayload": "{\"id\":\"urn:ngsi-ld:User:6ad19fe0-fc11-4024-85f2-931c6fa6f7e0\",\"type\":\"User\",\"roles\":{\"type\":\"Property\",\"value\":\"stellio-creator\"}}", + "contexts": [ + "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master/authorization/jsonld-contexts/authorization.jsonld", + "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" + ] +} From 92c62deefe08b2d0a41c60d595d2bb6ad2a6823b Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Mon, 27 Dec 2021 17:41:00 +0100 Subject: [PATCH 21/28] feat(search): implement entities filtering according to user's access rights --- search-service/config/detekt/baseline.xml | 2 +- .../service/AttributeInstanceService.kt | 2 +- .../service/EntityAccessRightsService.kt | 36 +++-- .../stellio/search/service/QueryService.kt | 6 +- .../service/SubjectReferentialService.kt | 16 +++ .../service/TemporalEntityAttributeService.kt | 66 +++++---- .../search/web/TemporalEntityHandler.kt | 26 ++-- .../web/TemporalEntityOperationsHandler.kt | 14 +- .../service/EntityAccessRightsServiceTests.kt | 68 +++++---- .../search/service/QueryServiceTests.kt | 16 +-- .../service/SubjectReferentialServiceTests.kt | 38 +++++ .../TemporalEntityAttributeServiceTests.kt | 81 ++++++++++- .../search/web/TemporalEntityHandlerTests.kt | 136 ++++++++++-------- .../TemporalEntityOperationsHandlerTests.kt | 29 ++-- 14 files changed, 362 insertions(+), 174 deletions(-) diff --git a/search-service/config/detekt/baseline.xml b/search-service/config/detekt/baseline.xml index a6bf5e45a..e5c4ca235 100644 --- a/search-service/config/detekt/baseline.xml +++ b/search-service/config/detekt/baseline.xml @@ -10,7 +10,7 @@ LongParameterList:AttributeInstance.kt$AttributeInstance.Companion$( temporalEntityAttribute: UUID, instanceId: URI? = null, observedAt: ZonedDateTime, value: String? = null, measuredValue: Double? = null, payload: Map<String, Any> ) LongParameterList:EntityEventListenerService.kt$EntityEventListenerService$( entityId: URI, attributeName: String, datasetId: URI?, operationPayload: JsonNode, updatedEntity: String, contexts: List<String> ) LongParameterList:EntityEventListenerService.kt$EntityEventListenerService$( entityId: URI, entityType: String, expandedAttributeName: String, operationPayload: JsonNode, updatedEntity: String, contexts: List<String> ) - LongParameterList:TemporalEntityAttributeService.kt$TemporalEntityAttributeService$( limit: Int, offset: Int, ids: Set<URI>, types: Set<String>, attrs: Set<String>, withEntityPayload: Boolean = false ) + LongParameterList:TemporalEntityAttributeService.kt$TemporalEntityAttributeService$( limit: Int, offset: Int, ids: Set<URI>, types: Set<String>, attrs: Set<String>, accessRightFilter: () -> String? ) ReturnCount:EntityEventListenerService.kt$EntityEventListenerService$internal fun toTemporalAttributeMetadata(operationPayload: JsonNode): Validated<String, AttributeMetadata> ReturnCount:TemporalEntityAttributeService.kt$TemporalEntityAttributeService$internal fun toTemporalAttributeMetadata( ngsiLdAttributeInstance: NgsiLdAttributeInstance ): Validated<String, AttributeMetadata> SwallowedException:TemporalEntityHandler.kt$e: IllegalArgumentException diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/AttributeInstanceService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/AttributeInstanceService.kt index ca13b776c..5f304da64 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/AttributeInstanceService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/AttributeInstanceService.kt @@ -35,7 +35,7 @@ class AttributeInstanceService( VALUES (:observed_at, :measured_value, :value, :temporal_entity_attribute, :instance_id, :payload) ON CONFLICT (observed_at, temporal_entity_attribute) DO UPDATE SET value = :value, measured_value = :measured_value, payload = :payload - """ + """.trimIndent() ) .bind("observed_at", attributeInstance.observedAt) .bind("measured_value", attributeInstance.measuredValue) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt index 978e2943b..5ec75c530 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt @@ -5,6 +5,7 @@ import com.egm.stellio.shared.util.AccessRight import com.egm.stellio.shared.util.AccessRight.R_CAN_ADMIN import com.egm.stellio.shared.util.AccessRight.R_CAN_READ import com.egm.stellio.shared.util.AccessRight.R_CAN_WRITE +import kotlinx.coroutines.reactive.awaitFirst import org.slf4j.LoggerFactory import org.springframework.data.r2dbc.core.R2dbcEntityTemplate import org.springframework.data.relational.core.query.Criteria @@ -83,22 +84,19 @@ class EntityAccessRightsService( // if user has stellio-admin role, no need to check further if (it) Mono.just(true) else { - // create a list with user id and its groups memberships ... - subjectReferentialService.retrieve(subjectId) - .map { subjectReferential -> - subjectReferential.groupsMemberships ?: emptyList() - } - .map { groups -> - groups.plus(subjectId) - } + subjectReferentialService.getSubjectAndGroupsUUID(subjectId) .flatMap { uuids -> // ... and check if it has the required role with at least one of them - hasRoleOnEntity(uuids, entityId, accessRights) + hasAccessRightOnEntity(uuids, entityId, accessRights) } } } - private fun hasRoleOnEntity(uuids: List, entityId: URI, accessRights: List): Mono = + private fun hasAccessRightOnEntity( + uuids: List, + entityId: URI, + accessRights: List + ): Mono = databaseClient .sql( """ @@ -122,6 +120,24 @@ class EntityAccessRightsService( Mono.just(false) } + suspend fun computeAccessRightFilter(subjectId: UUID): () -> String? { + if (subjectReferentialService.hasStellioAdminRole(subjectId).awaitFirst()) + return { null } + else { + val subjectAndGroupsUUID = subjectReferentialService.getSubjectAndGroupsUUID(subjectId).awaitFirst() + val formattedSubjectAndGroupsUUID = subjectAndGroupsUUID.joinToString(",") { "'$it'" } + return { + """ + entity_id IN ( + SELECT entity_id + FROM entity_access_rights + WHERE subject_id IN ($formattedSubjectAndGroupsUUID) + ) + """.trimIndent() + } + } + } + @Transactional fun delete(subjectId: UUID): Mono = r2dbcEntityTemplate.delete(EntityAccessRights::class.java) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/QueryService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/QueryService.kt index bb7381d96..f4c4310b7 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/QueryService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/QueryService.kt @@ -91,14 +91,16 @@ class QueryService( suspend fun queryTemporalEntities( temporalEntitiesQuery: TemporalEntitiesQuery, - contextLink: String + contextLink: String, + accessRightFilter: () -> String? ): List { val temporalEntityAttributes = temporalEntityAttributeService.getForEntities( temporalEntitiesQuery.limit, temporalEntitiesQuery.offset, temporalEntitiesQuery.ids, temporalEntitiesQuery.types, - temporalEntitiesQuery.temporalQuery.expandedAttrs + temporalEntitiesQuery.temporalQuery.expandedAttrs, + accessRightFilter ).awaitFirstOrDefault(emptyList()) val temporalEntityAttributesWithMatchingInstances = diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt index b1652291e..e73c776c6 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt @@ -59,6 +59,22 @@ class SubjectReferentialService( .one() .map { rowToSubjectReferential(it) } + fun getSubjectAndGroupsUUID(subjectId: UUID): Mono> = + databaseClient + .sql( + """ + SELECT groups_memberships + FROM subject_referential + WHERE subject_id = :subject_id + """.trimIndent() + ) + .bind("subject_id", subjectId) + .fetch() + .one() + .map { + ((it["groups_memberships"] as Array?)?.map { it.toUUID() } ?: emptyList()).plus(subjectId) + } + fun hasStellioAdminRole(subjectId: UUID): Mono = databaseClient .sql( diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeService.kt index a6fa6badd..0a7cc9cda 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeService.kt @@ -2,7 +2,6 @@ package com.egm.stellio.search.service import arrow.core.Validated import arrow.core.invalid -import arrow.core.orNull import arrow.core.valid import com.egm.stellio.search.config.ApplicationProperties import com.egm.stellio.search.model.AttributeInstance @@ -52,7 +51,7 @@ class TemporalEntityAttributeService( INSERT INTO temporal_entity_attribute (id, entity_id, type, attribute_name, attribute_type, attribute_value_type, dataset_id) VALUES (:id, :entity_id, :type, :attribute_name, :attribute_type, :attribute_value_type, :dataset_id) - """ + """.trimIndent() ) .bind("id", temporalEntityAttribute.id) .bind("entity_id", temporalEntityAttribute.entityId) @@ -70,7 +69,7 @@ class TemporalEntityAttributeService( """ INSERT INTO entity_payload (entity_id, payload) VALUES (:entity_id, :payload) - """ + """.trimIndent() ) .bind("entity_id", entityId) .bind("payload", entityPayload?.let { Json.of(entityPayload) }) @@ -81,7 +80,11 @@ class TemporalEntityAttributeService( fun updateEntityPayload(entityId: URI, payload: String): Mono = if (applicationProperties.entity.storePayloads) - databaseClient.sql("UPDATE entity_payload SET payload = :payload WHERE entity_id = :entity_id") + databaseClient.sql( + """ + UPDATE entity_payload SET payload = :payload WHERE entity_id = :entity_id + """.trimIndent() + ) .bind("payload", Json.of(payload)) .bind("entity_id", entityId) .fetch() @@ -90,7 +93,11 @@ class TemporalEntityAttributeService( Mono.just(1) fun deleteEntityPayload(entityId: URI): Mono = - databaseClient.sql("DELETE FROM entity_payload WHERE entity_id = :entity_id") + databaseClient.sql( + """ + DELETE FROM entity_payload WHERE entity_id = :entity_id + """.trimIndent() + ) .bind("entity_id", entityId) .fetch() .rowsUpdated() @@ -193,9 +200,9 @@ class TemporalEntityAttributeService( .zipWith( databaseClient.sql( """ - delete FROM temporal_entity_attribute WHERE - entity_id = :entity_id - AND attribute_name = :attribute_name + DELETE FROM temporal_entity_attribute + WHERE entity_id = :entity_id + AND attribute_name = :attribute_name """.trimIndent() ) .bind("entity_id", entityId) @@ -250,24 +257,16 @@ class TemporalEntityAttributeService( ids: Set, types: Set, attrs: Set, - withEntityPayload: Boolean = false + accessRightFilter: () -> String? ): Mono> { - val selectQuery = if (withEntityPayload) - """ - SELECT id, temporal_entity_attribute.entity_id, type, attribute_name, attribute_type, - attribute_value_type, payload::TEXT, dataset_id - FROM temporal_entity_attribute - LEFT JOIN entity_payload ON entity_payload.entity_id = temporal_entity_attribute.entity_id - WHERE - """.trimIndent() - else + val selectQuery = """ SELECT id, entity_id, type, attribute_name, attribute_type, attribute_value_type, dataset_id FROM temporal_entity_attribute WHERE """.trimIndent() - val filterQuery = buildEntitiesQueryFilter(ids, types, attrs) + val filterQuery = buildEntitiesQueryFilter(ids, types, attrs, accessRightFilter) val finalQuery = """ $selectQuery $filterQuery @@ -285,24 +284,30 @@ class TemporalEntityAttributeService( .collectList() } - fun getCountForEntities(ids: Set, types: Set, attrs: Set): Mono { + fun getCountForEntities( + ids: Set, + types: Set, + attrs: Set, + accessRightFilter: () -> String? + ): Mono { val selectStatement = """ SELECT count(distinct(entity_id)) as count_entity from temporal_entity_attribute WHERE """.trimIndent() - val filterQuery = buildEntitiesQueryFilter(ids, types, attrs) + val filterQuery = buildEntitiesQueryFilter(ids, types, attrs, accessRightFilter) return databaseClient .sql("$selectStatement $filterQuery") .map(rowToTemporalCount) - .first() + .one() } fun buildEntitiesQueryFilter( ids: Set, types: Set, - attrs: Set + attrs: Set, + accessRightFilter: () -> String?, ): String { val formattedIds = if (ids.isNotEmpty()) @@ -317,19 +322,12 @@ class TemporalEntityAttributeService( attrs.joinToString(separator = ",", prefix = "attribute_name in (", postfix = ")") { "'$it'" } else null - return listOfNotNull(formattedIds, formattedTypes, formattedAttrs).joinToString(" AND ") + return listOfNotNull(formattedIds, formattedTypes, formattedAttrs, accessRightFilter()) + .joinToString(" AND ") } - fun getForEntity(id: URI, attrs: Set, withEntityPayload: Boolean = false): Flux { - val selectQuery = if (withEntityPayload) - """ - SELECT id, temporal_entity_attribute.entity_id as entity_id, type, attribute_name, attribute_type, - attribute_value_type, payload::TEXT, dataset_id - FROM temporal_entity_attribute - LEFT JOIN entity_payload ON entity_payload.entity_id = temporal_entity_attribute.entity_id - WHERE temporal_entity_attribute.entity_id = :entity_id - """.trimIndent() - else + fun getForEntity(id: URI, attrs: Set): Flux { + val selectQuery = """ SELECT id, entity_id, type, attribute_name, attribute_type, attribute_value_type, dataset_id FROM temporal_entity_attribute diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt index a14cd4a5b..053bd50bd 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityHandler.kt @@ -38,7 +38,6 @@ import org.springframework.web.bind.annotation.RestController import reactor.core.publisher.Mono import java.time.ZonedDateTime import java.util.Optional -import java.util.UUID @RestController @RequestMapping("/ngsi-ld/v1/temporal/entities") @@ -58,9 +57,9 @@ class TemporalEntityHandler( @PathVariable entityId: String, @RequestBody requestBody: Mono ): ResponseEntity<*> { - val userId = extractSubjectOrEmpty().awaitFirst() + val userId = extractSubjectOrEmpty().awaitFirst().toUUID() val canWriteEntity = - entityAccessRightsService.canWriteEntity(UUID.fromString(userId), entityId.toUri()).awaitFirst() + entityAccessRightsService.canWriteEntity(userId, entityId.toUri()).awaitFirst() if (!canWriteEntity) throw AccessDeniedException("User forbidden write access to entity $entityId") @@ -100,19 +99,24 @@ class TemporalEntityHandler( @RequestHeader httpHeaders: HttpHeaders, @RequestParam params: MultiValueMap ): ResponseEntity<*> { + val userId = extractSubjectOrEmpty().awaitFirst().toUUID() + val count = params.getFirst(QUERY_PARAM_COUNT)?.toBoolean() ?: false val contextLink = getContextFromLinkHeaderOrDefault(httpHeaders) val mediaType = getApplicableMediaType(httpHeaders) val temporalEntitiesQuery = queryService.parseAndCheckQueryParams(params, contextLink) + val accessRightFilter = entityAccessRightsService.computeAccessRightFilter(userId) val temporalEntities = queryService.queryTemporalEntities( temporalEntitiesQuery, - contextLink + contextLink, + accessRightFilter ) val temporalEntityCount = temporalEntityAttributeService.getCountForEntities( temporalEntitiesQuery.ids, temporalEntitiesQuery.types, - temporalEntitiesQuery.temporalQuery.expandedAttrs + temporalEntitiesQuery.temporalQuery.expandedAttrs, + accessRightFilter ).awaitFirst() val prevAndNextLinks = PagingUtils.getPagingLinks( @@ -142,12 +146,7 @@ class TemporalEntityHandler( @PathVariable entityId: String, @RequestParam params: MultiValueMap ): ResponseEntity<*> { - val userId = extractSubjectOrEmpty().awaitFirst() - - val canReadEntity = - entityAccessRightsService.canReadEntity(UUID.fromString(userId), entityId.toUri()).awaitFirst() - if (!canReadEntity) - throw AccessDeniedException("User forbidden read access to entity $entityId") + val userId = extractSubjectOrEmpty().awaitFirst().toUUID() val withTemporalValues = hasValueInOptionsParam(Optional.ofNullable(params.getFirst("options")), OptionsParamValue.TEMPORAL_VALUES) @@ -161,6 +160,11 @@ class TemporalEntityHandler( .body(BadRequestDataResponse(e.message)) } + val canReadEntity = + entityAccessRightsService.canReadEntity(userId, entityId.toUri()).awaitFirst() + if (!canReadEntity) + throw AccessDeniedException("User forbidden read access to entity $entityId") + val temporalEntity = queryService.queryTemporalEntity(entityId.toUri(), temporalQuery, withTemporalValues, contextLink) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandler.kt index ad052583c..6b899b1d3 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandler.kt @@ -1,5 +1,6 @@ package com.egm.stellio.search.web +import com.egm.stellio.search.service.EntityAccessRightsService import com.egm.stellio.search.service.QueryService import com.egm.stellio.search.service.TemporalEntityAttributeService import com.egm.stellio.shared.util.* @@ -17,7 +18,8 @@ import reactor.core.publisher.Mono @RequestMapping("/ngsi-ld/v1/temporal/entityOperations") class TemporalEntityOperationsHandler( private val queryService: QueryService, - private val temporalEntityAttributeService: TemporalEntityAttributeService + private val temporalEntityAttributeService: TemporalEntityAttributeService, + private val entityAccessRightsService: EntityAccessRightsService ) { /** @@ -28,6 +30,8 @@ class TemporalEntityOperationsHandler( @RequestHeader httpHeaders: HttpHeaders, @RequestBody requestBody: Mono ): ResponseEntity<*> { + val userId = extractSubjectOrEmpty().awaitFirst().toUUID() + val contextLink = getContextFromLinkHeaderOrDefault(httpHeaders) val mediaType = getApplicableMediaType(httpHeaders) val body = requestBody.awaitFirst() @@ -41,14 +45,18 @@ class TemporalEntityOperationsHandler( } val temporalEntitiesQuery = queryService.parseAndCheckQueryParams(queryParams, contextLink) + + val accessRightFilter = entityAccessRightsService.computeAccessRightFilter(userId) val temporalEntities = queryService.queryTemporalEntities( temporalEntitiesQuery, - contextLink + contextLink, + accessRightFilter ) val temporalEntityCount = temporalEntityAttributeService.getCountForEntities( temporalEntitiesQuery.ids, temporalEntitiesQuery.types, - temporalEntitiesQuery.temporalQuery.expandedAttrs + temporalEntitiesQuery.temporalQuery.expandedAttrs, + accessRightFilter ).awaitFirst() val prevAndNextLinks = PagingUtils.getPagingLinks( diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityAccessRightsServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityAccessRightsServiceTests.kt index d4997bfea..f0e88b8db 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityAccessRightsServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityAccessRightsServiceTests.kt @@ -1,17 +1,17 @@ package com.egm.stellio.search.service import com.egm.stellio.search.model.EntityAccessRights -import com.egm.stellio.search.model.SubjectReferential import com.egm.stellio.search.support.WithTimescaleContainer import com.egm.stellio.shared.support.WithKafkaContainer -import com.egm.stellio.shared.util.SubjectType import com.egm.stellio.shared.util.toUri import com.ninjasquad.springmockk.MockkBean import io.mockk.Called import io.mockk.every -import io.mockk.mockkClass import io.mockk.verify +import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired @@ -38,13 +38,13 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer, WithKafkaContaine private val subjectUuid = UUID.fromString("0768A6D5-D87B-4209-9A22-8C40A8961A79") private val groupUuid = UUID.fromString("220FC854-3609-404B-BC77-F2DFE332B27B") private val entityId = "urn:ngsi-ld:Entity:1111".toUri() - private val defaultMockSubjectReferential = mockkClass(SubjectReferential::class) @BeforeEach fun setDefaultBehaviorOnSubjectReferential() { every { subjectReferentialService.hasStellioAdminRole(subjectUuid) } answers { Mono.just(false) } - every { subjectReferentialService.retrieve(subjectUuid) } answers { Mono.just(defaultMockSubjectReferential) } - every { defaultMockSubjectReferential.groupsMemberships } returns emptyList() + every { + subjectReferentialService.getSubjectAndGroupsUUID(subjectUuid) + } answers { Mono.just(listOf(subjectUuid)) } } @AfterEach @@ -126,16 +126,8 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer, WithKafkaContaine @Test fun `it should allow an user having a read role on a entity via a group membership`() { every { - subjectReferentialService.retrieve(subjectUuid) - } answers { - Mono.just( - SubjectReferential( - subjectId = subjectUuid, - subjectType = SubjectType.USER, - groupsMemberships = listOf(groupUuid, UUID.randomUUID()) - ) - ) - } + subjectReferentialService.getSubjectAndGroupsUUID(subjectUuid) + } answers { Mono.just(listOf(groupUuid, subjectUuid)) } entityAccessRightsService.setReadRoleOnEntity(groupUuid, entityId).block() @@ -155,16 +147,8 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer, WithKafkaContaine @Test fun `it should allow an user having a read role on a entity both directly and via a group membership`() { every { - subjectReferentialService.retrieve(subjectUuid) - } answers { - Mono.just( - SubjectReferential( - subjectId = subjectUuid, - subjectType = SubjectType.USER, - groupsMemberships = listOf(groupUuid, UUID.randomUUID()) - ) - ) - } + subjectReferentialService.getSubjectAndGroupsUUID(subjectUuid) + } answers { Mono.just(listOf(groupUuid, subjectUuid)) } entityAccessRightsService.setReadRoleOnEntity(groupUuid, entityId).block() entityAccessRightsService.setReadRoleOnEntity(subjectUuid, entityId).block() @@ -200,4 +184,36 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer, WithKafkaContaine verify { subjectReferentialService.retrieve(eq(subjectUuid)) wasNot Called } } + + @Test + fun `it should return a null filter is user has the stellio-admin role`() { + every { subjectReferentialService.hasStellioAdminRole(subjectUuid) } answers { Mono.just(true) } + + runBlocking { + val accessRightFilter = entityAccessRightsService.computeAccessRightFilter(subjectUuid) + assertNull(accessRightFilter()) + } + } + + @Test + fun `it should return a valid entity filter is user does not have the stellio-admin role`() { + every { subjectReferentialService.hasStellioAdminRole(subjectUuid) } answers { Mono.just(false) } + every { subjectReferentialService.getSubjectAndGroupsUUID(subjectUuid) } answers { + Mono.just(listOf(subjectUuid, groupUuid)) + } + + runBlocking { + val accessRightFilter = entityAccessRightsService.computeAccessRightFilter(subjectUuid) + assertEquals( + """ + entity_id IN ( + SELECT entity_id + FROM entity_access_rights + WHERE subject_id IN ('$subjectUuid','$groupUuid') + ) + """.trimIndent(), + accessRightFilter() + ) + } + } } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/QueryServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/QueryServiceTests.kt index 9948bff41..33c941a37 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/QueryServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/QueryServiceTests.kt @@ -193,7 +193,7 @@ class QueryServiceTests { ) verify { - temporalEntityAttributeService.getForEntity(entityUri, emptySet(), false) + temporalEntityAttributeService.getForEntity(entityUri, emptySet()) } verify { @@ -227,7 +227,7 @@ class QueryServiceTests { attributeName = "incoming", attributeValueType = TemporalEntityAttribute.AttributeValueType.MEASURE ) - every { temporalEntityAttributeService.getForEntities(any(), any(), any(), any(), any()) } returns + every { temporalEntityAttributeService.getForEntities(any(), any(), any(), any(), any(), any()) } returns Mono.just(listOf(temporalEntityAttribute)) every { attributeInstanceService.search(any(), any>(), any()) @@ -257,7 +257,7 @@ class QueryServiceTests { count = false ), APIC_COMPOUND_CONTEXT - ) + ) { null } verify { temporalEntityAttributeService.getForEntities( @@ -265,7 +265,8 @@ class QueryServiceTests { 2, emptySet(), setOf(beehiveType, apiaryType), - emptySet() + emptySet(), + any() ) } @@ -300,10 +301,7 @@ class QueryServiceTests { attributeValueType = TemporalEntityAttribute.AttributeValueType.MEASURE ) every { - temporalEntityAttributeService.getForEntities( - any(), any(), any(), any(), - any() - ) + temporalEntityAttributeService.getForEntities(any(), any(), any(), any(), any(), any()) } returns Mono.just( listOf(temporalEntityAttribute) ) @@ -328,7 +326,7 @@ class QueryServiceTests { false ), APIC_COMPOUND_CONTEXT - ) + ) { null } assertTrue(entitiesList.isEmpty()) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectReferentialServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectReferentialServiceTests.kt index a07965bc6..d39bf3000 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectReferentialServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/SubjectReferentialServiceTests.kt @@ -71,6 +71,44 @@ class SubjectReferentialServiceTests : WithTimescaleContainer, WithKafkaContaine .verify() } + @Test + fun `it should retrieve UUIDs from user and groups memberships`() { + val groupsUuids = List(3) { UUID.randomUUID() } + val subjectReferential = SubjectReferential( + subjectId = subjectUuid, + subjectType = SubjectType.USER, + groupsMemberships = groupsUuids + ) + + subjectReferentialService.create(subjectReferential).block() + + StepVerifier.create(subjectReferentialService.getSubjectAndGroupsUUID(subjectUuid)) + .expectNextMatches { + it.size == 4 && + it.containsAll(groupsUuids.plus(subjectUuid)) + } + .expectComplete() + .verify() + } + + @Test + fun `it should retrieve UUIDs from user when it has no groups memberships`() { + val subjectReferential = SubjectReferential( + subjectId = subjectUuid, + subjectType = SubjectType.USER + ) + + subjectReferentialService.create(subjectReferential).block() + + StepVerifier.create(subjectReferentialService.getSubjectAndGroupsUUID(subjectUuid)) + .expectNextMatches { + it.size == 1 && + it.contains(subjectUuid) + } + .expectComplete() + .verify() + } + @Test fun `it should update the global role of a subject`() { val subjectReferential = SubjectReferential( diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeServiceTests.kt index 34e4e85d5..586b77b12 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeServiceTests.kt @@ -320,7 +320,7 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon incomingAttrExpandedName, outgoingAttrExpandedName ) - ) + ) { null } StepVerifier.create(temporalEntityAttributes) .expectNextMatches { @@ -333,6 +333,7 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon .expectComplete() .verify() } + @Test fun `it should retrieve the temporal entities for the requested limit and offset`() { val firstRawEntity = loadSampleData("beehive_two_temporal_properties.jsonld") @@ -355,7 +356,7 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon incomingAttrExpandedName, outgoingAttrExpandedName ) - ) + ) { null } StepVerifier.create(temporalEntityAttributes) .expectNextCount(1) @@ -363,6 +364,42 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon .verify() } + @Test + fun `it should retrieve the persisted temporal attributes of the requested entities according to access rights`() { + val firstRawEntity = loadSampleData("beehive_two_temporal_properties.jsonld") + val secondRawEntity = loadSampleData("beehive.jsonld") + + every { attributeInstanceService.create(any()) } returns Mono.just(1) + + temporalEntityAttributeService.createEntityTemporalReferences(firstRawEntity, listOf(APIC_COMPOUND_CONTEXT)) + .block() + temporalEntityAttributeService.createEntityTemporalReferences(secondRawEntity, listOf(APIC_COMPOUND_CONTEXT)) + .block() + + val temporalEntityAttributes = + temporalEntityAttributeService.getForEntities( + 10, + 0, + setOf("urn:ngsi-ld:BeeHive:TESTD".toUri(), "urn:ngsi-ld:BeeHive:TESTC".toUri()), + setOf("https://ontology.eglobalmark.com/apic#BeeHive"), + setOf( + incomingAttrExpandedName, + outgoingAttrExpandedName + ) + ) { "entity_id IN ('urn:ngsi-ld:BeeHive:TESTD')" } + + StepVerifier.create(temporalEntityAttributes) + .expectNextMatches { + it.size == 2 && + it.all { tea -> + tea.type == "https://ontology.eglobalmark.com/apic#BeeHive" && + tea.entityId.toString() == "urn:ngsi-ld:BeeHive:TESTD" + } + } + .expectComplete() + .verify() + } + @Test fun `it should retrieve the persisted temporal entities count of the requested entities`() { val rawEntity = loadSampleData("beehive_two_temporal_properties.jsonld") @@ -383,10 +420,42 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon incomingAttrExpandedName, outgoingAttrExpandedName ) - ) + ) { null } StepVerifier.create(temporalEntity) - .expectNextCount(1) + .expectNextMatches { it == 1 } + .verifyComplete() + } + + @Test + fun `it should retrieve the temporal entities count of the requested entities according to access rights`() { + val rawEntity = loadSampleData("beehive_two_temporal_properties.jsonld") + + every { attributeInstanceService.create(any()) } returns Mono.just(1) + + temporalEntityAttributeService.createEntityTemporalReferences(rawEntity, listOf(APIC_COMPOUND_CONTEXT)) + .block() + + val temporalEntityNoResult = + temporalEntityAttributeService.getCountForEntities( + setOf("urn:ngsi-ld:BeeHive:TESTD".toUri(), "urn:ngsi-ld:BeeHive:TESTC".toUri()), + setOf("https://ontology.eglobalmark.com/apic#BeeHive"), + emptySet() + ) { "entity_id IN ('urn:ngsi-ld:BeeHive:TESTC')" } + + StepVerifier.create(temporalEntityNoResult) + .expectNextMatches { it == 0 } + .verifyComplete() + + val temporalEntityWithResult = + temporalEntityAttributeService.getCountForEntities( + setOf("urn:ngsi-ld:BeeHive:TESTD".toUri(), "urn:ngsi-ld:BeeHive:TESTC".toUri()), + setOf("https://ontology.eglobalmark.com/apic#BeeHive"), + emptySet() + ) { "entity_id IN ('urn:ngsi-ld:BeeHive:TESTD')" } + + StepVerifier.create(temporalEntityWithResult) + .expectNextMatches { it == 1 } .verifyComplete() } @@ -412,7 +481,7 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon incomingAttrExpandedName, outgoingAttrExpandedName ) - ) + ) { null } StepVerifier.create(temporalEntityAttributes) .expectNext(emptyList()) @@ -439,7 +508,7 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon setOf("urn:ngsi-ld:BeeHive:TESTD".toUri(), "urn:ngsi-ld:BeeHive:TESTC".toUri()), setOf("https://ontology.eglobalmark.com/apic#BeeHive"), setOf("unknownAttribute") - ) + ) { null } StepVerifier.create(temporalEntityAttributes) .expectNext(emptyList()) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt index 7d080b802..c4a99f68b 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityHandlerTests.kt @@ -19,6 +19,7 @@ import com.egm.stellio.shared.util.RESULTS_COUNT_HEADER import com.egm.stellio.shared.util.buildContextLinkHeader import com.egm.stellio.shared.util.entityNotFoundMessage import com.egm.stellio.shared.util.loadSampleData +import com.egm.stellio.shared.util.toUUID import com.egm.stellio.shared.util.toUri import com.ninjasquad.springmockk.MockkBean import io.mockk.Called @@ -302,8 +303,6 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if timerel is present without time query param`() { - every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } - webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=before") .exchange() @@ -321,8 +320,6 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if time is present without timerel query param`() { - every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } - webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?time=2020-10-29T18:00:00Z") .exchange() @@ -347,12 +344,14 @@ class TemporalEntityHandlerTests { .uri("/ngsi-ld/v1/temporal/entities/$entityUri") .exchange() .expectStatus().isOk + + coVerify { + entityAccessRightsService.canReadEntity(eq("0768A6D5-D87B-4209-9A22-8C40A8961A79".toUUID()), eq(entityUri)) + } } @Test fun `it should raise a 400 if timerel is between and no endTime provided`() { - every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } - webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=between&time=startTime") .exchange() @@ -370,8 +369,6 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if time is not parsable`() { - every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } - webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=before&time=badTime") .exchange() @@ -389,8 +386,6 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if timerel is not a valid value`() { - every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } - webClient.get() .uri("/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?timerel=befor&time=badTime") .exchange() @@ -408,8 +403,6 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if timerel is between and endTime is not parseable`() { - every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } - webClient.get() .uri( "/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?" + @@ -430,8 +423,6 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if one of time bucket or aggregate is missing`() { - every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } - webClient.get() .uri( "/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?" + @@ -452,8 +443,6 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 if aggregate function is unknown`() { - every { entityAccessRightsService.canReadEntity(any(), any()) } answers { Mono.just(true) } - webClient.get() .uri( "/ngsi-ld/v1/temporal/entities/urn:ngsi-ld:Entity:01?" + @@ -609,10 +598,11 @@ class TemporalEntityHandlerTests { "beehive_with_two_temporal_attributes_evolution.jsonld" val entityWith2temporalEvolutions = deserializeObject(loadSampleData(entityFileName)) - every { temporalEntityAttributeService.getForEntity(any(), any()) } returns Flux.just( - entityTemporalProperties[0], - entityTemporalProperties[1] - ) + every { + temporalEntityAttributeService.getForEntity(any(), any()) + } answers { + Flux.just(entityTemporalProperties[0], entityTemporalProperties[1]) + } val attributes = listOf( incomingAttrExpandedName, @@ -642,9 +632,9 @@ class TemporalEntityHandlerTests { @Test fun `it should raise a 400 and return an error response`() { - every { queryService.parseAndCheckQueryParams(any(), any()) } throws BadRequestDataException( - "'timerel' and 'time' must be used in conjunction" - ) + every { + queryService.parseAndCheckQueryParams(any(), any()) + } throws BadRequestDataException("'timerel' and 'time' must be used in conjunction") webClient.get() .uri("/ngsi-ld/v1/temporal/entities?timerel=before") @@ -669,13 +659,15 @@ class TemporalEntityHandlerTests { endTime = ZonedDateTime.parse("2019-10-18T07:31:39Z") ) - every { temporalEntityAttributeService.getCountForEntities(any(), any(), any()) } answers { Mono.just(2) } every { queryService.parseAndCheckQueryParams(any(), any()) } returns buildDefaultQueryParams().copy(types = setOf("BeeHive"), temporalQuery = temporalQuery) - + coEvery { entityAccessRightsService.computeAccessRightFilter(any()) } returns { null } coEvery { - queryService.queryTemporalEntities(any(), any()) + queryService.queryTemporalEntities(any(), any(), any()) } returns emptyList() + every { + temporalEntityAttributeService.getCountForEntities(any(), any(), any(), any()) + } answers { Mono.just(2) } webClient.get() .uri( @@ -709,7 +701,8 @@ class TemporalEntityHandlerTests { temporalEntitiesQuery.temporalQuery == temporalQuery && !temporalEntitiesQuery.withTemporalValues }, - eq(APIC_COMPOUND_CONTEXT) + eq(APIC_COMPOUND_CONTEXT), + any() ) } @@ -723,11 +716,14 @@ class TemporalEntityHandlerTests { ).minus("@context") val secondTemporalEntity = deserializeObject(loadSampleData("beehive.jsonld")).minus("@context") - every { temporalEntityAttributeService.getCountForEntities(any(), any(), any()) } answers { Mono.just(2) } every { queryService.parseAndCheckQueryParams(any(), any()) } returns buildDefaultQueryParams() + coEvery { entityAccessRightsService.computeAccessRightFilter(any()) } returns { null } coEvery { - queryService.queryTemporalEntities(any(), any()) + queryService.queryTemporalEntities(any(), any(), any()) } returns listOf(firstTemporalEntity, secondTemporalEntity) + every { + temporalEntityAttributeService.getCountForEntities(any(), any(), any(), any()) + } answers { Mono.just(2) } webClient.get() .uri( @@ -752,11 +748,14 @@ class TemporalEntityHandlerTests { ).minus("@context") val secondTemporalEntity = deserializeObject(loadSampleData("beehive.jsonld")).minus("@context") - every { temporalEntityAttributeService.getCountForEntities(any(), any(), any()) } answers { Mono.just(2) } every { queryService.parseAndCheckQueryParams(any(), any()) } returns buildDefaultQueryParams() + coEvery { entityAccessRightsService.computeAccessRightFilter(any()) } returns { null } coEvery { - queryService.queryTemporalEntities(any(), any()) + queryService.queryTemporalEntities(any(), any(), any()) } returns listOf(firstTemporalEntity, secondTemporalEntity) + every { + temporalEntityAttributeService.getCountForEntities(any(), any(), any(), any()) + } answers { Mono.just(2) } webClient.get() .uri( @@ -883,13 +882,16 @@ class TemporalEntityHandlerTests { ).minus("@context") val secondTemporalEntity = deserializeObject(loadSampleData("beehive.jsonld")).minus("@context") - every { temporalEntityAttributeService.getCountForEntities(any(), any(), any()) } returns Mono.just(2) - every { queryService.parseAndCheckQueryParams(any(), any()) } returns - buildDefaultQueryParams().copy(limit = 1, offset = 2) + every { + queryService.parseAndCheckQueryParams(any(), any()) + } returns buildDefaultQueryParams().copy(limit = 1, offset = 2) + coEvery { entityAccessRightsService.computeAccessRightFilter(any()) } returns { null } coEvery { - queryService.queryTemporalEntities(any(), any()) - } returns - listOf(firstTemporalEntity, secondTemporalEntity) + queryService.queryTemporalEntities(any(), any(), any()) + } returns listOf(firstTemporalEntity, secondTemporalEntity) + every { + temporalEntityAttributeService.getCountForEntities(any(), any(), any(), any()) + } answers { Mono.just(2) } webClient.get() .uri( @@ -909,11 +911,13 @@ class TemporalEntityHandlerTests { } @Test - fun `query temporal entity should return 200 and empty response if requested offset does not exists`() { - - every { temporalEntityAttributeService.getCountForEntities(any(), any(), any()) } returns Mono.just(2) + fun `query temporal entity should return 200 and empty response if requested offset does not exist`() { every { queryService.parseAndCheckQueryParams(any(), any()) } returns buildDefaultQueryParams() - coEvery { queryService.queryTemporalEntities(any(), any()) } returns emptyList() + coEvery { entityAccessRightsService.computeAccessRightFilter(any()) } returns { null } + coEvery { queryService.queryTemporalEntities(any(), any(), any()) } returns emptyList() + every { + temporalEntityAttributeService.getCountForEntities(any(), any(), any(), any()) + } answers { Mono.just(2) } webClient.get() .uri( @@ -928,10 +932,12 @@ class TemporalEntityHandlerTests { @Test fun `query temporal entities should return 200 and the number of results if count is asked for`() { - - every { temporalEntityAttributeService.getCountForEntities(any(), any(), any()) } returns Mono.just(2) every { queryService.parseAndCheckQueryParams(any(), any()) } returns buildDefaultQueryParams() - coEvery { queryService.queryTemporalEntities(any(), any()) } returns emptyList() + coEvery { entityAccessRightsService.computeAccessRightFilter(any()) } returns { null } + coEvery { queryService.queryTemporalEntities(any(), any(), any()) } returns emptyList() + every { + temporalEntityAttributeService.getCountForEntities(any(), any(), any(), any()) + } answers { Mono.just(2) } webClient.get() .uri( @@ -952,13 +958,16 @@ class TemporalEntityHandlerTests { ).minus("@context") val secondTemporalEntity = deserializeObject(loadSampleData("beehive.jsonld")).minus("@context") - every { temporalEntityAttributeService.getCountForEntities(any(), any(), any()) } returns Mono.just(2) - every { queryService.parseAndCheckQueryParams(any(), any()) } returns - buildDefaultQueryParams().copy(limit = 1, offset = 0) + every { + queryService.parseAndCheckQueryParams(any(), any()) + } returns buildDefaultQueryParams().copy(limit = 1, offset = 0) + coEvery { entityAccessRightsService.computeAccessRightFilter(any()) } returns { null } coEvery { - queryService.queryTemporalEntities(any(), any()) - } returns - listOf(firstTemporalEntity, secondTemporalEntity) + queryService.queryTemporalEntities(any(), any(), any()) + } returns listOf(firstTemporalEntity, secondTemporalEntity) + every { + temporalEntityAttributeService.getCountForEntities(any(), any(), any(), any()) + } answers { Mono.just(2) } webClient.get() .uri( @@ -984,13 +993,16 @@ class TemporalEntityHandlerTests { ).minus("@context") val secondTemporalEntity = deserializeObject(loadSampleData("beehive.jsonld")).minus("@context") - every { temporalEntityAttributeService.getCountForEntities(any(), any(), any()) } returns Mono.just(3) - every { queryService.parseAndCheckQueryParams(any(), any()) } returns - buildDefaultQueryParams().copy(limit = 1, offset = 1) + every { + queryService.parseAndCheckQueryParams(any(), any()) + } returns buildDefaultQueryParams().copy(limit = 1, offset = 1) + coEvery { entityAccessRightsService.computeAccessRightFilter(any()) } returns { null } coEvery { - queryService.queryTemporalEntities(any(), any()) - } returns - listOf(firstTemporalEntity, secondTemporalEntity) + queryService.queryTemporalEntities(any(), any(), any()) + } returns listOf(firstTemporalEntity, secondTemporalEntity) + every { + temporalEntityAttributeService.getCountForEntities(any(), any(), any(), any()) + } answers { Mono.just(3) } webClient.get() .uri( @@ -1014,10 +1026,10 @@ class TemporalEntityHandlerTests { @Test fun `query temporal entity should return 400 if requested offset is less than zero`() { - every { queryService.parseAndCheckQueryParams(any(), any()) } returns buildDefaultQueryParams() + coEvery { entityAccessRightsService.computeAccessRightFilter(any()) } returns { null } coEvery { - queryService.queryTemporalEntities(any(), any()) + queryService.queryTemporalEntities(any(), any(), any()) } throws BadRequestDataException( "Offset must be greater than zero and limit must be strictly greater than zero" ) @@ -1043,10 +1055,10 @@ class TemporalEntityHandlerTests { @Test fun `query temporal entity should return 400 if limit is equal or less than zero`() { - every { queryService.parseAndCheckQueryParams(any(), any()) } returns buildDefaultQueryParams() + coEvery { entityAccessRightsService.computeAccessRightFilter(any()) } returns { null } coEvery { - queryService.queryTemporalEntities(any(), any()) + queryService.queryTemporalEntities(any(), any(), any()) } throws BadRequestDataException( "Offset must be greater than zero and limit must be strictly greater than zero" ) @@ -1072,10 +1084,10 @@ class TemporalEntityHandlerTests { @Test fun `query temporal entity should return 400 if limit is greater than the maximum authorized limit`() { - every { queryService.parseAndCheckQueryParams(any(), any()) } returns buildDefaultQueryParams() + coEvery { entityAccessRightsService.computeAccessRightFilter(any()) } returns { null } coEvery { - queryService.queryTemporalEntities(any(), any()) + queryService.queryTemporalEntities(any(), any(), any()) } throws BadRequestDataException( "You asked for 200 results, but the supported maximum limit is 100" ) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandlerTests.kt index 3e37e19b2..0949eb04d 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandlerTests.kt @@ -3,8 +3,10 @@ package com.egm.stellio.search.web import com.egm.stellio.search.config.WebSecurityTestConfig import com.egm.stellio.search.model.TemporalEntitiesQuery import com.egm.stellio.search.model.TemporalQuery +import com.egm.stellio.search.service.EntityAccessRightsService import com.egm.stellio.search.service.QueryService import com.egm.stellio.search.service.TemporalEntityAttributeService +import com.egm.stellio.shared.WithMockCustomUser import com.egm.stellio.shared.model.BadRequestDataException import com.egm.stellio.shared.util.* import com.ninjasquad.springmockk.MockkBean @@ -15,7 +17,6 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest import org.springframework.context.annotation.Import import org.springframework.security.test.context.support.WithAnonymousUser -import org.springframework.security.test.context.support.WithMockUser import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.util.LinkedMultiValueMap @@ -26,7 +27,7 @@ import java.time.ZonedDateTime @ActiveProfiles("test") @WebFluxTest(TemporalEntityOperationsHandler::class) @Import(WebSecurityTestConfig::class) -@WithMockUser +@WithMockCustomUser(name = "Mock User", username = "0768A6D5-D87B-4209-9A22-8C40A8961A79") class TemporalEntityOperationsHandlerTests { private lateinit var apicHeaderLink: String @@ -40,6 +41,9 @@ class TemporalEntityOperationsHandlerTests { @MockkBean(relaxed = true) private lateinit var queryService: QueryService + @MockkBean + private lateinit var entityAccessRightsService: EntityAccessRightsService + @BeforeAll fun configureWebClientDefaults() { apicHeaderLink = buildContextLinkHeader(APIC_COMPOUND_CONTEXT) @@ -63,7 +67,6 @@ class TemporalEntityOperationsHandlerTests { expandedAttrs = setOf(incomingAttrExpandedName, outgoingAttrExpandedName) ) - every { temporalEntityAttributeService.getCountForEntities(any(), any(), any()) } answers { Mono.just(2) } every { queryService.parseAndCheckQueryParams(any(), any()) } returns TemporalEntitiesQuery( ids = emptySet(), @@ -74,7 +77,11 @@ class TemporalEntityOperationsHandlerTests { offset = 0, false ) - coEvery { queryService.queryTemporalEntities(any(), any()) } returns emptyList() + coEvery { entityAccessRightsService.computeAccessRightFilter(any()) } returns { null } + coEvery { queryService.queryTemporalEntities(any(), any(), any()) } returns emptyList() + every { + temporalEntityAttributeService.getCountForEntities(any(), any(), any(), any()) + } answers { Mono.just(2) } val queryParams = LinkedMultiValueMap() queryParams.add("options", "temporalValues") @@ -107,7 +114,8 @@ class TemporalEntityOperationsHandlerTests { temporalEntitiesQuery.temporalQuery == temporalQuery && temporalEntitiesQuery.withTemporalValues }, - eq(APIC_COMPOUND_CONTEXT) + eq(APIC_COMPOUND_CONTEXT), + any() ) } @@ -123,7 +131,6 @@ class TemporalEntityOperationsHandlerTests { expandedAttrs = setOf(incomingAttrExpandedName, outgoingAttrExpandedName) ) - every { temporalEntityAttributeService.getCountForEntities(any(), any(), any()) } answers { Mono.just(2) } every { queryService.parseAndCheckQueryParams(any(), any()) } returns TemporalEntitiesQuery( ids = emptySet(), @@ -134,6 +141,10 @@ class TemporalEntityOperationsHandlerTests { offset = 1, true ) + coEvery { entityAccessRightsService.computeAccessRightFilter(any()) } returns { null } + every { + temporalEntityAttributeService.getCountForEntities(any(), any(), any(), any()) + } answers { Mono.just(2) } val queryParams = LinkedMultiValueMap() queryParams.add("options", "temporalValues") @@ -165,10 +176,10 @@ class TemporalEntityOperationsHandlerTests { temporalEntitiesQuery.ids.isEmpty() && temporalEntitiesQuery.types == setOf("BeeHive", "Apiary") && temporalEntitiesQuery.temporalQuery == temporalQuery && - temporalEntitiesQuery.withTemporalValues && - temporalEntitiesQuery.count == true + temporalEntitiesQuery.withTemporalValues && temporalEntitiesQuery.count }, - eq(APIC_COMPOUND_CONTEXT) + eq(APIC_COMPOUND_CONTEXT), + any() ) } From c2090d3931bb97509d363fbe59243351d4bc826c Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Sun, 2 Jan 2022 08:36:00 +0100 Subject: [PATCH 22/28] feat: implement iam and rights synchronizer --- .../Neo4jAuthorizationService.kt | 2 + .../egm/stellio/entity/web/IAMSynchronizer.kt | 142 ++++++++++++++++++ .../egm/stellio/search/service/IAMListener.kt | 90 ++++++++--- .../service/SubjectReferentialService.kt | 20 ++- ...add_index_on_temporal_entity_attribute.sql | 1 + .../search/service/IAMListenerTests.kt | 20 +-- .../egm/stellio/shared/model/JsonLdEntity.kt | 3 - .../com/egm/stellio/shared/util/AuthUtils.kt | 6 +- 8 files changed, 246 insertions(+), 38 deletions(-) create mode 100644 entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt create mode 100644 search-service/src/main/resources/db/migration/V0_18__add_index_on_temporal_entity_attribute.sql diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt index dc2571db5..dffffe1c2 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt @@ -1,5 +1,6 @@ package com.egm.stellio.entity.authorization +import arrow.core.flattenOption import com.egm.stellio.entity.authorization.AuthorizationService.Companion.ADMIN_RIGHT import com.egm.stellio.entity.authorization.AuthorizationService.Companion.READ_RIGHT import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_ADMIN @@ -28,6 +29,7 @@ class Neo4jAuthorizationService( private fun userIsOneOfGivenRoles(roles: Set, userSub: String): Boolean = neo4jAuthorizationRepository.getUserRoles((USER_PREFIX + userSub).toUri()) .map { GlobalRole.forKey(it) } + .flattenOption() .intersect(roles) .isNotEmpty() diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt new file mode 100644 index 000000000..915276230 --- /dev/null +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt @@ -0,0 +1,142 @@ +package com.egm.stellio.entity.web + +import com.egm.stellio.entity.authorization.AuthorizationService +import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_ADMIN +import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_READ +import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_WRITE +import com.egm.stellio.entity.service.EntityService +import com.egm.stellio.shared.model.AttributeAppendEvent +import com.egm.stellio.shared.model.EntityCreateEvent +import com.egm.stellio.shared.model.JsonLdEntity +import com.egm.stellio.shared.model.QueryParams +import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_ID +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CORE_CONTEXT +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DATASET_ID_PROPERTY +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_EGM_AUTHORIZATION_CONTEXT +import com.egm.stellio.shared.util.JsonLdUtils.compactAndSerialize +import com.egm.stellio.shared.util.JsonLdUtils.compactFragment +import com.egm.stellio.shared.util.JsonUtils.serializeObject +import com.egm.stellio.shared.util.extractSubjectOrEmpty +import com.egm.stellio.shared.util.toUri +import kotlinx.coroutines.reactive.awaitFirst +import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/admin") +class IAMSynchronizer( + private val entityService: EntityService, + private val authorizationService: AuthorizationService, + private val kafkaTemplate: KafkaTemplate, +) { + private val logger = LoggerFactory.getLogger(javaClass) + + @PostMapping("/iam/sync") + suspend fun syncIam(): ResponseEntity<*> { + val userId = extractSubjectOrEmpty().awaitFirst() + if (!authorizationService.userIsAdmin(userId)) + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body("User is not authorized to sync user referential") + + val authorizationContexts = listOf(NGSILD_EGM_AUTHORIZATION_CONTEXT, NGSILD_CORE_CONTEXT) + listOf(AuthorizationService.USER_LABEL, AuthorizationService.GROUP_LABEL, AuthorizationService.CLIENT_LABEL) + .asSequence() + .map { + // do a first search without asking for a result in order to get the total count + val total = entityService.searchEntities( + QueryParams(expandedType = it), + userId, + 0, + 0, + NGSILD_EGM_AUTHORIZATION_CONTEXT, + false + ).first + entityService.searchEntities( + QueryParams(expandedType = it), + userId, + 0, + total, + NGSILD_EGM_AUTHORIZATION_CONTEXT, + false + ) + } + .map { it.second } + .flatten() + .map { jsonLdEntity -> + val entitiesRightsEvents = + generateAttributeAppendEvents(jsonLdEntity, authorizationContexts, R_CAN_ADMIN) + .plus(generateAttributeAppendEvents(jsonLdEntity, authorizationContexts, R_CAN_WRITE)) + .plus(generateAttributeAppendEvents(jsonLdEntity, authorizationContexts, R_CAN_READ)) + + val updatedEntity = compactAndSerialize( + jsonLdEntity.copy( + properties = jsonLdEntity.properties.minus(listOf(R_CAN_ADMIN, R_CAN_WRITE, R_CAN_READ)), + ), + authorizationContexts, + MediaType.APPLICATION_JSON + ) + val iamEvent = EntityCreateEvent( + jsonLdEntity.id.toUri(), + jsonLdEntity.type.substringAfterLast("#"), + updatedEntity, + authorizationContexts + ) + listOf(iamEvent).plus(entitiesRightsEvents) + } + .flatten() + .toList() + .forEach { + val serializedEvent = serializeObject(it) + logger.debug("Sending event: $serializedEvent") + kafkaTemplate.send("cim.iam.replay", it.entityId.toString(), serializedEvent) + } + + return ResponseEntity.status(HttpStatus.NO_CONTENT).build() + } + + private fun generateAttributeAppendEvents( + jsonLdEntity: JsonLdEntity, + authorizationContexts: List, + accessRight: String + ) = if (jsonLdEntity.properties.containsKey(accessRight)) { + when (val rCanAdmin = jsonLdEntity.properties[accessRight]) { + is Map<*, *> -> + listOf( + AttributeAppendEvent( + jsonLdEntity.id.toUri(), + jsonLdEntity.type.substringAfterLast("#"), + accessRight.substringAfterLast("#"), + (rCanAdmin[NGSILD_DATASET_ID_PROPERTY] as String).toUri(), + true, + serializeObject(compactFragment(rCanAdmin as Map, authorizationContexts)), + "", + authorizationContexts + ) + ) + is List<*> -> + rCanAdmin.map { rCanAdminItem -> + rCanAdminItem as Map + AttributeAppendEvent( + jsonLdEntity.id.toUri(), + jsonLdEntity.type.substringAfterLast("#"), + accessRight.substringAfterLast("#"), + ((rCanAdminItem[NGSILD_DATASET_ID_PROPERTY] as Map)[JSONLD_ID] as String).toUri(), + true, + serializeObject(compactFragment(rCanAdminItem, authorizationContexts)), + "", + authorizationContexts + ) + } + else -> { + logger.warn("Unsupported representation for $accessRight: $rCanAdmin") + emptyList() + } + } + } else emptyList() +} diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt index 392ef23e6..a423b37c9 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt @@ -1,5 +1,6 @@ package com.egm.stellio.search.service +import arrow.core.flattenOption import com.egm.stellio.search.model.SubjectReferential import com.egm.stellio.shared.model.AttributeAppendEvent import com.egm.stellio.shared.model.AttributeDeleteEvent @@ -13,12 +14,15 @@ import com.egm.stellio.shared.util.JsonUtils import com.egm.stellio.shared.util.SubjectType import com.egm.stellio.shared.util.extractSubjectUuid import com.egm.stellio.shared.util.toUri +import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.ObjectNode +import com.fasterxml.jackson.databind.node.TextNode import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import org.slf4j.LoggerFactory import org.springframework.kafka.annotation.KafkaListener import org.springframework.stereotype.Component +import java.util.UUID @Component class IAMListener( @@ -29,7 +33,8 @@ class IAMListener( private val logger = LoggerFactory.getLogger(javaClass) @KafkaListener(topics = ["cim.iam"], groupId = "search-iam") - fun processMessage(content: String) { + fun processIam(content: String) { + logger.debug("Received event: $content") when (val authorizationEvent = JsonUtils.deserializeAs(content)) { is EntityCreateEvent -> createSubjectReferential(authorizationEvent) is EntityDeleteEvent -> deleteSubjectReferential(authorizationEvent) @@ -49,17 +54,45 @@ class IAMListener( } } + @KafkaListener(topics = ["cim.iam.replay"], groupId = "search-iam-replay") + fun processIamReplay(content: String) { + logger.debug("Received event: $content") + when (val authorizationEvent = JsonUtils.deserializeAs(content)) { + is EntityCreateEvent -> createFullSubjectReferential(authorizationEvent) + is AttributeAppendEvent -> addEntityToSubject(authorizationEvent) + else -> logger.info("Authorization event ${authorizationEvent.operationType} not handled.") + } + } + + private fun createFullSubjectReferential(entityCreateEvent: EntityCreateEvent) { + val operationPayloadNode = jacksonObjectMapper().readTree(entityCreateEvent.operationPayload) + val roles = extractRoles(operationPayloadNode) + val serviceAccountId = + if (operationPayloadNode.has("serviceAccountId")) + (operationPayloadNode["serviceAccountId"] as ObjectNode)["value"].asText() + else null + val groupsMemberships = extractGroupsMemberships(operationPayloadNode) + val subjectReferential = SubjectReferential( + subjectId = entityCreateEvent.entityId.extractSubjectUuid(), + subjectType = SubjectType.valueOf(entityCreateEvent.entityType.uppercase()), + globalRoles = roles, + serviceAccountId = serviceAccountId?.extractSubjectUuid(), + groupsMemberships = groupsMemberships + ) + + subjectReferentialService.create(subjectReferential) + .subscribe { + logger.debug("Created subject ${entityCreateEvent.entityId}") + } + } + private fun createSubjectReferential(entityCreateEvent: EntityCreateEvent) { val operationPayloadNode = jacksonObjectMapper().readTree(entityCreateEvent.operationPayload) - val defaultRole = - if (operationPayloadNode.has("roles")) { - val roleAsText = (operationPayloadNode["roles"] as ObjectNode)["value"].asText() - listOf(GlobalRole.forKey(roleAsText)) - } else null + val roles = extractRoles(operationPayloadNode) val subjectReferential = SubjectReferential( subjectId = entityCreateEvent.entityId.extractSubjectUuid(), subjectType = SubjectType.valueOf(entityCreateEvent.entityType.uppercase()), - globalRoles = defaultRole + globalRoles = roles ) subjectReferentialService.create(subjectReferential) @@ -68,6 +101,30 @@ class IAMListener( } } + private fun extractGroupsMemberships(operationPayloadNode: JsonNode): List? = + if (operationPayloadNode.has("isMemberOf")) { + when (val isMemberOf = operationPayloadNode["isMemberOf"]) { + is ObjectNode -> listOf(isMemberOf["object"].asText().extractSubjectUuid()) + is ArrayNode -> + isMemberOf.map { + it["object"].asText().extractSubjectUuid() + }.ifEmpty { null } + else -> null + } + } else null + + private fun extractRoles(operationPayloadNode: JsonNode): List? = + if (operationPayloadNode.has("roles")) { + when (val rolesValue = (operationPayloadNode["roles"] as ObjectNode)["value"]) { + is TextNode -> GlobalRole.forKey(rolesValue.asText()).map { listOf(it) }.orNull() + is ArrayNode -> + rolesValue.map { + GlobalRole.forKey(it.asText()) + }.flattenOption() + else -> null + } + } else null + private fun deleteSubjectReferential(entityDeleteEvent: EntityDeleteEvent) { subjectReferentialService.delete(entityDeleteEvent.entityId.extractSubjectUuid()) .subscribe { @@ -79,26 +136,25 @@ class IAMListener( val operationPayloadNode = jacksonObjectMapper().readTree(attributeAppendEvent.operationPayload) val subjectUuid = attributeAppendEvent.entityId.extractSubjectUuid() if (attributeAppendEvent.attributeName == "roles") { - val updatedRoles = (operationPayloadNode["value"] as ArrayNode).elements() - val newRoles = updatedRoles.asSequence().map { + val newRoles = (operationPayloadNode["value"] as ArrayNode).map { GlobalRole.forKey(it.asText()) - }.toList() + }.flattenOption() if (newRoles.isNotEmpty()) - subjectReferentialService.setGlobalRoles(subjectUuid, newRoles) + subjectReferentialService.setGlobalRoles(subjectUuid, newRoles).subscribe() else - subjectReferentialService.resetGlobalRoles(subjectUuid) + subjectReferentialService.resetGlobalRoles(subjectUuid).subscribe() } else if (attributeAppendEvent.attributeName == "serviceAccountId") { val serviceAccountId = operationPayloadNode["value"].asText() subjectReferentialService.addServiceAccountIdToClient( subjectUuid, serviceAccountId.extractSubjectUuid() - ) + ).subscribe() } else if (attributeAppendEvent.attributeName == "isMemberOf") { val groupId = operationPayloadNode["object"].asText() subjectReferentialService.addGroupMembershipToUser( subjectUuid, groupId.extractSubjectUuid() - ) + ).subscribe() } else { logger.info("Received unknown attribute name: ${attributeAppendEvent.attributeName}") } @@ -108,7 +164,7 @@ class IAMListener( subjectReferentialService.removeGroupMembershipToUser( attributeDeleteEvent.entityId.extractSubjectUuid(), attributeDeleteEvent.datasetId!!.extractSubjectUuid() - ) + ).subscribe() } private fun addEntityToSubject(attributeAppendEvent: AttributeAppendEvent) { @@ -118,13 +174,13 @@ class IAMListener( attributeAppendEvent.entityId.extractSubjectUuid(), entityId.toUri(), AccessRight.forAttributeName(attributeAppendEvent.attributeName) - ) + ).subscribe() } private fun removeEntityFromSubject(attributeDeleteEvent: AttributeDeleteEvent) { entityAccessRightsService.removeRoleOnEntity( attributeDeleteEvent.entityId.extractSubjectUuid(), attributeDeleteEvent.attributeName.toUri() - ) + ).subscribe() } } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt index e73c776c6..ae0d51b19 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt @@ -1,5 +1,6 @@ package com.egm.stellio.search.service +import arrow.core.getOrElse import com.egm.stellio.search.model.SubjectReferential import com.egm.stellio.shared.util.GlobalRole import com.egm.stellio.shared.util.SubjectType @@ -29,12 +30,17 @@ class SubjectReferentialService( .sql( """ INSERT INTO subject_referential - (subject_id, subject_type, global_roles, groups_memberships) - VALUES (:subject_id, :subject_type, :global_roles, :groups_memberships) + (subject_id, subject_type, service_account_id, global_roles, groups_memberships) + VALUES (:subject_id, :subject_type, :service_account_id, :global_roles, :groups_memberships) + ON CONFLICT (subject_id) + DO UPDATE SET service_account_id = :service_account_id, + global_roles = :global_roles, + groups_memberships = :groups_memberships """.trimIndent() ) .bind("subject_id", subjectReferential.subjectId) .bind("subject_type", subjectReferential.subjectType.toString()) + .bind("service_account_id", subjectReferential.serviceAccountId) .bind("global_roles", subjectReferential.globalRoles?.map { it.key }?.toTypedArray()) .bind("groups_memberships", subjectReferential.groupsMemberships?.toTypedArray()) .fetch() @@ -63,16 +69,17 @@ class SubjectReferentialService( databaseClient .sql( """ - SELECT groups_memberships + SELECT subject_id, groups_memberships FROM subject_referential - WHERE subject_id = :subject_id + WHERE (subject_id = :subject_id OR service_account_id = :subject_id) """.trimIndent() ) .bind("subject_id", subjectId) .fetch() .one() .map { - ((it["groups_memberships"] as Array?)?.map { it.toUUID() } ?: emptyList()).plus(subjectId) + ((it["groups_memberships"] as Array?)?.map { it.toUUID() } ?: emptyList()) + .plus(it["subject_id"] as UUID) } fun hasStellioAdminRole(subjectId: UUID): Mono = @@ -203,7 +210,8 @@ class SubjectReferentialService( subjectId = row["subject_id"] as UUID, subjectType = SubjectType.valueOf(row["subject_type"] as String), serviceAccountId = row["service_account_id"] as UUID?, - globalRoles = (row["global_roles"] as Array?)?.map { GlobalRole.forKey(it) }, + globalRoles = (row["global_roles"] as Array?) + ?.mapNotNull { GlobalRole.forKey(it).getOrElse { null } }, groupsMemberships = (row["groups_memberships"] as Array?)?.map { it.toUUID() } ) } diff --git a/search-service/src/main/resources/db/migration/V0_18__add_index_on_temporal_entity_attribute.sql b/search-service/src/main/resources/db/migration/V0_18__add_index_on_temporal_entity_attribute.sql new file mode 100644 index 000000000..221786d0e --- /dev/null +++ b/search-service/src/main/resources/db/migration/V0_18__add_index_on_temporal_entity_attribute.sql @@ -0,0 +1 @@ +CREATE INDEX ON attribute_instance(temporal_entity_attribute); diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt index 6d5efcc96..2ed51a5f2 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt @@ -31,7 +31,7 @@ class IAMListenerTests { fun `it should handle a create event for a subject`() { val subjectCreateEvent = loadSampleData("events/authorization/UserCreateEvent.json") - iamListener.processMessage(subjectCreateEvent) + iamListener.processIam(subjectCreateEvent) verify { subjectReferentialService.create( @@ -49,7 +49,7 @@ class IAMListenerTests { fun `it should handle a create event for a subject with a default role`() { val subjectCreateEvent = loadSampleData("events/authorization/UserCreateEventDefaultRole.json") - iamListener.processMessage(subjectCreateEvent) + iamListener.processIam(subjectCreateEvent) verify { subjectReferentialService.create( @@ -67,7 +67,7 @@ class IAMListenerTests { fun `it should handle a delete event for a subject`() { val subjectDeleteEvent = loadSampleData("events/authorization/UserDeleteEvent.json") - iamListener.processMessage(subjectDeleteEvent) + iamListener.processIam(subjectDeleteEvent) verify { subjectReferentialService.delete( @@ -83,7 +83,7 @@ class IAMListenerTests { fun `it should handle an append event adding a stellio-admin role for a group`() { val roleAppendEvent = loadSampleData("events/authorization/RealmRoleAppendEventOneRole.json") - iamListener.processMessage(roleAppendEvent) + iamListener.processIam(roleAppendEvent) verify { subjectReferentialService.setGlobalRoles( @@ -100,7 +100,7 @@ class IAMListenerTests { fun `it should handle an append event adding a stellio-admin role for a client`() { val roleAppendEvent = loadSampleData("events/authorization/RealmRoleAppendToClient.json") - iamListener.processMessage(roleAppendEvent) + iamListener.processIam(roleAppendEvent) verify { subjectReferentialService.setGlobalRoles( @@ -117,7 +117,7 @@ class IAMListenerTests { fun `it should handle an append event adding a stellio-admin role within two roles`() { val roleAppendEvent = loadSampleData("events/authorization/RealmRoleAppendEventTwoRoles.json") - iamListener.processMessage(roleAppendEvent) + iamListener.processIam(roleAppendEvent) verify { subjectReferentialService.setGlobalRoles( @@ -134,7 +134,7 @@ class IAMListenerTests { fun `it should handle an append event removing a stellio-admin role for a group`() { val roleAppendEvent = loadSampleData("events/authorization/RealmRoleAppendEventNoRole.json") - iamListener.processMessage(roleAppendEvent) + iamListener.processIam(roleAppendEvent) verify { subjectReferentialService.resetGlobalRoles( @@ -150,7 +150,7 @@ class IAMListenerTests { fun `it should handle an append event adding an user to a group`() { val roleAppendEvent = loadSampleData("events/authorization/GroupMembershipAppendEvent.json") - iamListener.processMessage(roleAppendEvent) + iamListener.processIam(roleAppendEvent) verify { subjectReferentialService.addGroupMembershipToUser( @@ -169,7 +169,7 @@ class IAMListenerTests { fun `it should handle a delete event removing an user from a group`() { val roleAppendEvent = loadSampleData("events/authorization/GroupMembershipDeleteEvent.json") - iamListener.processMessage(roleAppendEvent) + iamListener.processIam(roleAppendEvent) verify { subjectReferentialService.removeGroupMembershipToUser( @@ -188,7 +188,7 @@ class IAMListenerTests { fun `it should handle an append event adding a service account id to a client`() { val roleAppendEvent = loadSampleData("events/authorization/ServiceAccountIdAppendEvent.json") - iamListener.processMessage(roleAppendEvent) + iamListener.processIam(roleAppendEvent) verify { subjectReferentialService.addServiceAccountIdToClient( diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/model/JsonLdEntity.kt b/shared/src/main/kotlin/com/egm/stellio/shared/model/JsonLdEntity.kt index 9f7b1f0d3..4a314ba24 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/model/JsonLdEntity.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/model/JsonLdEntity.kt @@ -25,6 +25,3 @@ data class JsonLdEntity( types.firstOrNull() ?: "" } } - -fun CompactedJsonLdEntity.getType(): String = - this["type"] as String diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt index d066cd8ee..0c7cf1d1f 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt @@ -1,5 +1,7 @@ package com.egm.stellio.shared.util +import arrow.core.Option +import arrow.core.toOption import com.egm.stellio.shared.util.GlobalRole.STELLIO_ADMIN import com.egm.stellio.shared.util.GlobalRole.STELLIO_CREATOR import org.springframework.security.core.context.ReactiveSecurityContextHolder @@ -38,8 +40,8 @@ enum class GlobalRole(val key: String) { STELLIO_ADMIN("stellio-admin"); companion object { - fun forKey(key: String): GlobalRole = - values().find { it.key == key } ?: throw IllegalArgumentException("Unrecognized key $key") + fun forKey(key: String): Option = + values().find { it.key == key }.toOption() } } From 7ca6485b09314d7e41c965d57ad716a16df3baa3 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Sun, 2 Jan 2022 09:51:03 +0100 Subject: [PATCH 23/28] feat(api-gw): add route for IAM sync endpoint --- .../apigateway/ApiGatewayApplication.kt | 41 +++++-------------- .../egm/stellio/entity/web/IAMSynchronizer.kt | 2 +- 2 files changed, 12 insertions(+), 31 deletions(-) diff --git a/api-gateway/src/main/kotlin/com/egm/stellio/apigateway/ApiGatewayApplication.kt b/api-gateway/src/main/kotlin/com/egm/stellio/apigateway/ApiGatewayApplication.kt index 47e35d6bb..722625a9f 100644 --- a/api-gateway/src/main/kotlin/com/egm/stellio/apigateway/ApiGatewayApplication.kt +++ b/api-gateway/src/main/kotlin/com/egm/stellio/apigateway/ApiGatewayApplication.kt @@ -22,42 +22,23 @@ class ApiGatewayApplication { fun myRoutes(builder: RouteLocatorBuilder): RouteLocator { return builder.routes() .route { p -> - p.path("/ngsi-ld/v1/entities/**") + p.path( + "/ngsi-ld/v1/entities/**", + "/ngsi-ld/v1/entityOperations/**", + "/ngsi-ld/v1/entityAccessControl/**", + "/ngsi-ld/v1/types/**", + "/entity/admin/**" + ) .filters { it.tokenRelay() } .uri("http://$entityServiceUrl:8082") } .route { p -> - p.path("/ngsi-ld/v1/entityOperations/**") - .filters { - it.tokenRelay() - } - .uri("http://$entityServiceUrl:8082") - } - .route { p -> - p.path("/ngsi-ld/v1/entityAccessControl/**") - .filters { - it.tokenRelay() - } - .uri("http://$entityServiceUrl:8082") - } - .route { p -> - p.path("/ngsi-ld/v1/types/**") - .filters { - it.tokenRelay() - } - .uri("http://$entityServiceUrl:8082") - } - .route { p -> - p.path("/ngsi-ld/v1/temporal/entities/**") - .filters { - it.tokenRelay() - } - .uri("http://$searchServiceUrl:8083") - } - .route { p -> - p.path("/ngsi-ld/v1/temporal/entityOperations/**") + p.path( + "/ngsi-ld/v1/temporal/entities/**", + "/ngsi-ld/v1/temporal/entityOperations/**" + ) .filters { it.tokenRelay() } diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt index 915276230..ece7cfc48 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt @@ -29,7 +29,7 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping("/admin") +@RequestMapping("/entity/admin") class IAMSynchronizer( private val entityService: EntityService, private val authorizationService: AuthorizationService, From e311aff1605f3e379bcbf13a90909b9e3a893c4a Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Sun, 2 Jan 2022 14:39:38 +0100 Subject: [PATCH 24/28] fix(entity): reuse common constants in ApiBootstrapper --- .../authorization/AuthorizationService.kt | 3 +- .../Neo4jAuthorizationRepository.kt | 10 +++---- .../entity/service/EntityEventService.kt | 4 +-- .../entity/util/ApiTestsBootstrapper.kt | 29 +++++++++---------- .../Neo4jAuthorizationRepositoryTest.kt | 20 ++++++------- .../repository/Neo4jSearchRepositoryTests.kt | 4 +-- 6 files changed, 33 insertions(+), 37 deletions(-) diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt index 86e7c0db8..bf456bcb8 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt @@ -11,7 +11,8 @@ interface AuthorizationService { const val GROUP_LABEL = AUTHORIZATION_ONTOLOGY + "Group" const val CLIENT_LABEL = AUTHORIZATION_ONTOLOGY + "Client" val IAM_LABELS = setOf(USER_LABEL, GROUP_LABEL, CLIENT_LABEL) - const val EGM_ROLES = AUTHORIZATION_ONTOLOGY + "roles" + const val AUTHZ_PROP_ROLES = AUTHORIZATION_ONTOLOGY + "roles" + const val AUTHZ_PROP_USERNAME = AUTHORIZATION_ONTOLOGY + "username" const val R_CAN_READ = AUTHORIZATION_ONTOLOGY + "rCanRead" const val R_CAN_WRITE = AUTHORIZATION_ONTOLOGY + "rCanWrite" const val R_CAN_ADMIN = AUTHORIZATION_ONTOLOGY + "rCanAdmin" diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt index 86b844faa..e7dc4fc07 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt @@ -1,7 +1,7 @@ package com.egm.stellio.entity.authorization +import com.egm.stellio.entity.authorization.AuthorizationService.Companion.AUTHZ_PROP_ROLES import com.egm.stellio.entity.authorization.AuthorizationService.Companion.CLIENT_LABEL -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.EGM_ROLES import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_ADMIN import com.egm.stellio.entity.authorization.AuthorizationService.Companion.SERVICE_ACCOUNT_ID import com.egm.stellio.entity.authorization.AuthorizationService.Companion.USER_LABEL @@ -80,15 +80,15 @@ class Neo4jAuthorizationRepository( val query = """ MATCH (userEntity:Entity { id: ${'$'}userId }) - OPTIONAL MATCH (userEntity)-[:HAS_VALUE]->(p:Property { name:"$EGM_ROLES" }) + OPTIONAL MATCH (userEntity)-[:HAS_VALUE]->(p:Property { name:"$AUTHZ_PROP_ROLES" }) OPTIONAL MATCH (userEntity)-[:HAS_OBJECT]-(r:Attribute:Relationship)- - [:isMemberOf]->(group:Entity)-[:HAS_VALUE]->(pgroup:Property { name: "$EGM_ROLES" }) + [:isMemberOf]->(group:Entity)-[:HAS_VALUE]->(pgroup:Property { name: "$AUTHZ_PROP_ROLES" }) RETURN apoc.coll.union(collect(p.value), collect(pgroup.value)) as roles UNION MATCH (client:Entity)-[:HAS_VALUE]->(sid:Property { name: "$SERVICE_ACCOUNT_ID", value: ${'$'}userId }) - OPTIONAL MATCH (client)-[:HAS_VALUE]->(p:Property { name:"$EGM_ROLES" }) + OPTIONAL MATCH (client)-[:HAS_VALUE]->(p:Property { name:"$AUTHZ_PROP_ROLES" }) OPTIONAL MATCH (client)-[:HAS_OBJECT]-(r:Attribute:Relationship)- - [:isMemberOf]->(group:Entity)-[:HAS_VALUE]->(pgroup:Property { name: "$EGM_ROLES" }) + [:isMemberOf]->(group:Entity)-[:HAS_VALUE]->(pgroup:Property { name: "$AUTHZ_PROP_ROLES" }) RETURN apoc.coll.union(collect(p.value), collect(pgroup.value)) as roles """.trimIndent() diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/service/EntityEventService.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/service/EntityEventService.kt index 2963491a0..ad4fa8253 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/service/EntityEventService.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/service/EntityEventService.kt @@ -40,8 +40,6 @@ class EntityEventService( private val logger = LoggerFactory.getLogger(javaClass) - private val iamTopic = "cim.iam.rights" - internal fun composeTopicName(entityType: String): Validated { val topicName = entityChannelName(entityType) return try { @@ -67,7 +65,7 @@ class EntityEventService( private fun entityChannelName(entityType: String) = if (AuthorizationService.IAM_LABELS.contains(entityType)) - iamTopic + "cim.iam.rights" else "cim.entity.$entityType" diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt index deb7f8bf6..32b5dfd96 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt @@ -1,12 +1,15 @@ package com.egm.stellio.entity.util -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.AUTHORIZATION_ONTOLOGY +import com.egm.stellio.entity.authorization.AuthorizationService.Companion.AUTHZ_PROP_ROLES +import com.egm.stellio.entity.authorization.AuthorizationService.Companion.AUTHZ_PROP_USERNAME +import com.egm.stellio.entity.authorization.AuthorizationService.Companion.USER_LABEL import com.egm.stellio.entity.authorization.AuthorizationService.Companion.USER_PREFIX import com.egm.stellio.entity.model.Entity import com.egm.stellio.entity.model.Property import com.egm.stellio.entity.repository.EntityRepository import com.egm.stellio.shared.util.GlobalRole -import com.egm.stellio.shared.util.JsonLdUtils.EGM_BASE_CONTEXT_URL +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CORE_CONTEXT +import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_EGM_AUTHORIZATION_CONTEXT import com.egm.stellio.shared.util.toUri import org.springframework.beans.factory.annotation.Value import org.springframework.boot.CommandLineRunner @@ -22,15 +25,6 @@ class ApiTestsBootstrapper( @Value("\${application.apitests.userid}") val apiTestUserId: String? = null - companion object { - val AUTHORIZATION_CONTEXTS: List = listOf( - "$EGM_BASE_CONTEXT_URL/authorization/jsonld-contexts/authorization.jsonld", - "http://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld" - ) - const val USER_TYPE = "User" - val USER_ROLES = listOf(GlobalRole.STELLIO_CREATOR.key) - } - override fun run(vararg args: String?) { // well, this should not happen in api-tests profile as we start from a fresh database on each run val ngsiLdUserId = (USER_PREFIX + apiTestUserId!!).toUri() @@ -38,15 +32,18 @@ class ApiTestsBootstrapper( if (apiTestsUser == null) { val entity = Entity( id = ngsiLdUserId, - type = listOf(AUTHORIZATION_ONTOLOGY + USER_TYPE), - contexts = AUTHORIZATION_CONTEXTS, + type = listOf(USER_LABEL), + contexts = listOf( + NGSILD_EGM_AUTHORIZATION_CONTEXT, + NGSILD_CORE_CONTEXT + ), properties = mutableListOf( Property( - name = AUTHORIZATION_ONTOLOGY + "roles", - value = USER_ROLES + name = AUTHZ_PROP_ROLES, + value = listOf(GlobalRole.STELLIO_CREATOR.key) ), Property( - name = AUTHORIZATION_ONTOLOGY + "username", + name = AUTHZ_PROP_USERNAME, value = "API Tests" ) ) diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt index 89f0a4b89..e3c9e107c 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt @@ -1,14 +1,14 @@ package com.egm.stellio.entity.authorization -import com.egm.stellio.entity.authorization.AuthorizationService.* +import com.egm.stellio.entity.authorization.AuthorizationService.Companion.AUTHZ_PROP_ROLES import com.egm.stellio.entity.authorization.AuthorizationService.Companion.CLIENT_LABEL -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.EGM_ROLES import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_ADMIN import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_READ import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_WRITE import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_IS_MEMBER_OF import com.egm.stellio.entity.authorization.AuthorizationService.Companion.SERVICE_ACCOUNT_ID import com.egm.stellio.entity.authorization.AuthorizationService.Companion.USER_LABEL +import com.egm.stellio.entity.authorization.AuthorizationService.SpecificAccessPolicy import com.egm.stellio.entity.config.WithNeo4jContainer import com.egm.stellio.entity.model.Entity import com.egm.stellio.entity.model.Property @@ -240,7 +240,7 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer listOf(USER_LABEL), mutableListOf( Property( - name = EGM_ROLES, + name = AUTHZ_PROP_ROLES, value = listOf("admin", "creator") ) ) @@ -258,7 +258,7 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer listOf(CLIENT_LABEL), mutableListOf( Property( - name = EGM_ROLES, + name = AUTHZ_PROP_ROLES, value = listOf("admin", "creator") ), Property( @@ -280,7 +280,7 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer listOf(USER_LABEL), mutableListOf( Property( - name = EGM_ROLES, + name = AUTHZ_PROP_ROLES, value = "admin" ) ) @@ -300,7 +300,7 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer listOf("Group"), mutableListOf( Property( - name = EGM_ROLES, + name = AUTHZ_PROP_ROLES, value = listOf("admin") ) ) @@ -320,7 +320,7 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer listOf(USER_LABEL), mutableListOf( Property( - name = EGM_ROLES, + name = AUTHZ_PROP_ROLES, value = "admin" ) ) @@ -331,7 +331,7 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer listOf("Group"), mutableListOf( Property( - name = EGM_ROLES, + name = AUTHZ_PROP_ROLES, value = listOf("creator") ) ) @@ -353,7 +353,7 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer listOf("Group"), mutableListOf( Property( - name = EGM_ROLES, + name = AUTHZ_PROP_ROLES, value = "admin" ) ) @@ -404,7 +404,7 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer value = "some-uuid" ), Property( - name = EGM_ROLES, + name = AUTHZ_PROP_ROLES, value = listOf("admin", "creator") ) ) diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepositoryTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepositoryTests.kt index 3e0968db3..e902bf39c 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepositoryTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepositoryTests.kt @@ -1,7 +1,7 @@ package com.egm.stellio.entity.repository import com.egm.stellio.entity.authorization.AuthorizationService -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.EGM_ROLES +import com.egm.stellio.entity.authorization.AuthorizationService.Companion.AUTHZ_PROP_ROLES import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_ADMIN import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_READ import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_WRITE @@ -198,7 +198,7 @@ class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { listOf(AuthorizationService.USER_LABEL), mutableListOf( Property( - name = EGM_ROLES, + name = AUTHZ_PROP_ROLES, value = "admin" ) ) From 21b41ecff2be77046375237618e03b1d2cf1ac39 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Sun, 2 Jan 2022 15:13:04 +0100 Subject: [PATCH 25/28] refactor(entity): cleanup and refactor IAMSynchronizer --- .../authorization/AuthorizationService.kt | 3 + .../entity/web/EntityAccessControlHandler.kt | 2 +- .../egm/stellio/entity/web/IAMSynchronizer.kt | 84 ++++++++++--------- 3 files changed, 49 insertions(+), 40 deletions(-) diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt index bf456bcb8..b27071bc0 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt @@ -22,6 +22,9 @@ interface AuthorizationService { val ADMIN_RIGHT: Set = setOf(R_CAN_ADMIN) val WRITE_RIGHT: Set = setOf(R_CAN_WRITE).plus(ADMIN_RIGHT) val READ_RIGHT: Set = setOf(R_CAN_READ).plus(WRITE_RIGHT) + + // specific to authz terms where we know the compacted term is what is after the last # character + fun String.toCompactTerm(): String = this.substringAfterLast("#") } enum class SpecificAccessPolicy { diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt index e30b518d1..bc264592c 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt @@ -129,6 +129,6 @@ class EntityAccessControlHandler( return if (removeResult != 0) ResponseEntity.status(HttpStatus.NO_CONTENT).build() else - throw ResourceNotFoundException("Subject $subjectId has no right on entity $entityId") + throw ResourceNotFoundException("No right found for subject $subjectId on entity $entityId") } } diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt index ece7cfc48..05885c90d 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt @@ -4,9 +4,11 @@ import com.egm.stellio.entity.authorization.AuthorizationService import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_ADMIN import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_READ import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_WRITE +import com.egm.stellio.entity.authorization.AuthorizationService.Companion.toCompactTerm import com.egm.stellio.entity.service.EntityService import com.egm.stellio.shared.model.AttributeAppendEvent import com.egm.stellio.shared.model.EntityCreateEvent +import com.egm.stellio.shared.model.ExpandedTerm import com.egm.stellio.shared.model.JsonLdEntity import com.egm.stellio.shared.model.QueryParams import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_ID @@ -69,11 +71,13 @@ class IAMSynchronizer( .map { it.second } .flatten() .map { jsonLdEntity -> + // generate an attribute append event per rCanXXX relationship val entitiesRightsEvents = - generateAttributeAppendEvents(jsonLdEntity, authorizationContexts, R_CAN_ADMIN) - .plus(generateAttributeAppendEvents(jsonLdEntity, authorizationContexts, R_CAN_WRITE)) - .plus(generateAttributeAppendEvents(jsonLdEntity, authorizationContexts, R_CAN_READ)) + generateAttributeAppendEvents(jsonLdEntity, R_CAN_ADMIN, authorizationContexts) + .plus(generateAttributeAppendEvents(jsonLdEntity, R_CAN_WRITE, authorizationContexts)) + .plus(generateAttributeAppendEvents(jsonLdEntity, R_CAN_READ, authorizationContexts)) + // remove the rCanXXX relationships as they are sent separately val updatedEntity = compactAndSerialize( jsonLdEntity.copy( properties = jsonLdEntity.properties.minus(listOf(R_CAN_ADMIN, R_CAN_WRITE, R_CAN_READ)), @@ -83,7 +87,7 @@ class IAMSynchronizer( ) val iamEvent = EntityCreateEvent( jsonLdEntity.id.toUri(), - jsonLdEntity.type.substringAfterLast("#"), + jsonLdEntity.type.toCompactTerm(), updatedEntity, authorizationContexts ) @@ -102,41 +106,43 @@ class IAMSynchronizer( private fun generateAttributeAppendEvents( jsonLdEntity: JsonLdEntity, - authorizationContexts: List, - accessRight: String - ) = if (jsonLdEntity.properties.containsKey(accessRight)) { - when (val rCanAdmin = jsonLdEntity.properties[accessRight]) { - is Map<*, *> -> - listOf( - AttributeAppendEvent( - jsonLdEntity.id.toUri(), - jsonLdEntity.type.substringAfterLast("#"), - accessRight.substringAfterLast("#"), - (rCanAdmin[NGSILD_DATASET_ID_PROPERTY] as String).toUri(), - true, - serializeObject(compactFragment(rCanAdmin as Map, authorizationContexts)), - "", - authorizationContexts - ) - ) - is List<*> -> - rCanAdmin.map { rCanAdminItem -> - rCanAdminItem as Map - AttributeAppendEvent( - jsonLdEntity.id.toUri(), - jsonLdEntity.type.substringAfterLast("#"), - accessRight.substringAfterLast("#"), - ((rCanAdminItem[NGSILD_DATASET_ID_PROPERTY] as Map)[JSONLD_ID] as String).toUri(), - true, - serializeObject(compactFragment(rCanAdminItem, authorizationContexts)), - "", - authorizationContexts - ) + accessRight: ExpandedTerm, + authorizationContexts: List + ): List = + if (jsonLdEntity.properties.containsKey(accessRight)) { + when (val rightRel = jsonLdEntity.properties[accessRight]) { + is Map<*, *> -> + listOf(rightRelToAttributeAppendEvent(jsonLdEntity, rightRel, accessRight, authorizationContexts)) + is List<*> -> + rightRel.map { rightRelInstance -> + rightRelToAttributeAppendEvent( + jsonLdEntity, + rightRelInstance as Map<*, *>, + accessRight, + authorizationContexts + ) + } + else -> { + logger.warn("Unsupported representation for $accessRight: $rightRel") + emptyList() } - else -> { - logger.warn("Unsupported representation for $accessRight: $rCanAdmin") - emptyList() } - } - } else emptyList() + } else emptyList() + + private fun rightRelToAttributeAppendEvent( + jsonLdEntity: JsonLdEntity, + rightRel: Map<*, *>, + accessRight: ExpandedTerm, + authorizationContexts: List + ): AttributeAppendEvent = + AttributeAppendEvent( + jsonLdEntity.id.toUri(), + jsonLdEntity.type.toCompactTerm(), + accessRight.toCompactTerm(), + ((rightRel[NGSILD_DATASET_ID_PROPERTY] as Map)[JSONLD_ID] as String).toUri(), + true, + serializeObject(compactFragment(rightRel as Map, authorizationContexts)), + "", + authorizationContexts + ) } From bce65c59b7267fe29d1a6e5cf4aa34ab0827a2b6 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Sun, 2 Jan 2022 17:28:03 +0100 Subject: [PATCH 26/28] refactor: move security context related variable in shared module --- .../authorization/AuthorizationService.kt | 29 ----- .../Neo4jAuthorizationRepository.kt | 30 ++--- .../Neo4jAuthorizationService.kt | 26 ++-- .../entity/repository/Neo4jRepository.kt | 16 +-- .../repository/Neo4jSearchRepository.kt | 2 +- .../stellio/entity/repository/QueryUtils.kt | 20 +-- .../entity/service/EntityEventService.kt | 4 +- .../entity/util/ApiTestsBootstrapper.kt | 14 +-- .../entity/web/EntityAccessControlHandler.kt | 5 +- .../egm/stellio/entity/web/IAMSynchronizer.kt | 23 ++-- .../Neo4jAuthorizationRepositoryTest.kt | 116 +++++++++--------- .../Neo4jAuthorizationServiceTest.kt | 15 +-- .../repository/Neo4jSearchRepositoryTests.kt | 82 +++++++------ .../web/EntityAccessControlHandlerTests.kt | 18 +-- .../service/EntityAccessRightsService.kt | 25 ++-- .../egm/stellio/search/service/IAMListener.kt | 39 +++--- .../com/egm/stellio/shared/util/AuthUtils.kt | 39 +++++- .../egm/stellio/shared/util/JsonLdUtils.kt | 1 + 18 files changed, 264 insertions(+), 240 deletions(-) diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt index b27071bc0..b20ded000 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/AuthorizationService.kt @@ -3,35 +3,6 @@ package com.egm.stellio.entity.authorization import java.net.URI interface AuthorizationService { - - companion object { - const val USER_PREFIX: String = "urn:ngsi-ld:User:" - const val AUTHORIZATION_ONTOLOGY = "https://ontology.eglobalmark.com/authorization#" - const val USER_LABEL = AUTHORIZATION_ONTOLOGY + "User" - const val GROUP_LABEL = AUTHORIZATION_ONTOLOGY + "Group" - const val CLIENT_LABEL = AUTHORIZATION_ONTOLOGY + "Client" - val IAM_LABELS = setOf(USER_LABEL, GROUP_LABEL, CLIENT_LABEL) - const val AUTHZ_PROP_ROLES = AUTHORIZATION_ONTOLOGY + "roles" - const val AUTHZ_PROP_USERNAME = AUTHORIZATION_ONTOLOGY + "username" - const val R_CAN_READ = AUTHORIZATION_ONTOLOGY + "rCanRead" - const val R_CAN_WRITE = AUTHORIZATION_ONTOLOGY + "rCanWrite" - const val R_CAN_ADMIN = AUTHORIZATION_ONTOLOGY + "rCanAdmin" - val IAM_RIGHTS = setOf(R_CAN_READ, R_CAN_WRITE, R_CAN_ADMIN) - const val R_IS_MEMBER_OF = AUTHORIZATION_ONTOLOGY + "isMemberOf" - const val SERVICE_ACCOUNT_ID = AUTHORIZATION_ONTOLOGY + "serviceAccountId" - val ADMIN_RIGHT: Set = setOf(R_CAN_ADMIN) - val WRITE_RIGHT: Set = setOf(R_CAN_WRITE).plus(ADMIN_RIGHT) - val READ_RIGHT: Set = setOf(R_CAN_READ).plus(WRITE_RIGHT) - - // specific to authz terms where we know the compacted term is what is after the last # character - fun String.toCompactTerm(): String = this.substringAfterLast("#") - } - - enum class SpecificAccessPolicy { - AUTH_READ, - AUTH_WRITE - } - fun userIsAdmin(userSub: String): Boolean fun userCanCreateEntities(userSub: String): Boolean fun filterEntitiesUserCanRead(entitiesId: List, userSub: String): List diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt index e7dc4fc07..2a7850eee 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt @@ -1,11 +1,11 @@ package com.egm.stellio.entity.authorization -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.AUTHZ_PROP_ROLES -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.CLIENT_LABEL -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_ADMIN -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.SERVICE_ACCOUNT_ID -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.USER_LABEL import com.egm.stellio.entity.model.Relationship +import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_ROLES +import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_SID +import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_ADMIN +import com.egm.stellio.shared.util.AuthContextModel.CLIENT_TYPE +import com.egm.stellio.shared.util.AuthContextModel.USER_TYPE import com.egm.stellio.shared.util.JsonLdUtils.EGM_SPECIFIC_ACCESS_POLICY import com.egm.stellio.shared.util.toListOfString import com.egm.stellio.shared.util.toUri @@ -27,7 +27,7 @@ class Neo4jAuthorizationRepository( """ MATCH (userEntity:Entity) WHERE (userEntity.id = ${'$'}userId - OR (userEntity)-[:HAS_VALUE]->(:Property { name: "$SERVICE_ACCOUNT_ID", value: ${'$'}userId })) + OR (userEntity)-[:HAS_VALUE]->(:Property { name: "$AUTH_PROP_SID", value: ${'$'}userId })) WITH userEntity MATCH (entity:Entity) WHERE entity.id IN ${'$'}entitiesId @@ -80,15 +80,15 @@ class Neo4jAuthorizationRepository( val query = """ MATCH (userEntity:Entity { id: ${'$'}userId }) - OPTIONAL MATCH (userEntity)-[:HAS_VALUE]->(p:Property { name:"$AUTHZ_PROP_ROLES" }) + OPTIONAL MATCH (userEntity)-[:HAS_VALUE]->(p:Property { name:"$AUTH_PROP_ROLES" }) OPTIONAL MATCH (userEntity)-[:HAS_OBJECT]-(r:Attribute:Relationship)- - [:isMemberOf]->(group:Entity)-[:HAS_VALUE]->(pgroup:Property { name: "$AUTHZ_PROP_ROLES" }) + [:isMemberOf]->(group:Entity)-[:HAS_VALUE]->(pgroup:Property { name: "$AUTH_PROP_ROLES" }) RETURN apoc.coll.union(collect(p.value), collect(pgroup.value)) as roles UNION - MATCH (client:Entity)-[:HAS_VALUE]->(sid:Property { name: "$SERVICE_ACCOUNT_ID", value: ${'$'}userId }) - OPTIONAL MATCH (client)-[:HAS_VALUE]->(p:Property { name:"$AUTHZ_PROP_ROLES" }) + MATCH (client:Entity)-[:HAS_VALUE]->(sid:Property { name: "$AUTH_PROP_SID", value: ${'$'}userId }) + OPTIONAL MATCH (client)-[:HAS_VALUE]->(p:Property { name:"$AUTH_PROP_ROLES" }) OPTIONAL MATCH (client)-[:HAS_OBJECT]-(r:Attribute:Relationship)- - [:isMemberOf]->(group:Entity)-[:HAS_VALUE]->(pgroup:Property { name: "$AUTHZ_PROP_ROLES" }) + [:isMemberOf]->(group:Entity)-[:HAS_VALUE]->(pgroup:Property { name: "$AUTH_PROP_ROLES" }) RETURN apoc.coll.union(collect(p.value), collect(pgroup.value)) as roles """.trimIndent() @@ -116,18 +116,18 @@ class Neo4jAuthorizationRepository( val query = """ CALL { - MATCH (user:Entity:`$USER_LABEL`) + MATCH (user:Entity:`$USER_TYPE`) WHERE user.id = ${'$'}userId RETURN user UNION - MATCH (user:Entity:`$CLIENT_LABEL`) - WHERE (user)-[:HAS_VALUE]->(:Property { name: "$SERVICE_ACCOUNT_ID", value: ${'$'}userId }) + MATCH (user:Entity:`$CLIENT_TYPE`) + WHERE (user)-[:HAS_VALUE]->(:Property { name: "$AUTH_PROP_SID", value: ${'$'}userId }) RETURN user } WITH user UNWIND ${'$'}relPropsAndTargets AS relPropAndTarget MATCH (target:Entity { id: relPropAndTarget.targetEntityId }) - CREATE (user)-[:HAS_OBJECT]->(r:Attribute:Relationship:`$R_CAN_ADMIN`)-[:rCanAdmin]->(target) + CREATE (user)-[:HAS_OBJECT]->(r:Attribute:Relationship:`$AUTH_REL_CAN_ADMIN`)-[:rCanAdmin]->(target) SET r = relPropAndTarget.props RETURN r.id as id """ diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt index dffffe1c2..e24dc3dcf 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationService.kt @@ -1,14 +1,14 @@ package com.egm.stellio.entity.authorization import arrow.core.flattenOption -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.ADMIN_RIGHT -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.READ_RIGHT -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_ADMIN -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.USER_PREFIX -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.WRITE_RIGHT -import com.egm.stellio.entity.authorization.AuthorizationService.SpecificAccessPolicy import com.egm.stellio.entity.model.Relationship import com.egm.stellio.shared.util.ADMIN_ROLES +import com.egm.stellio.shared.util.AuthContextModel.ADMIN_RIGHTS +import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_ADMIN +import com.egm.stellio.shared.util.AuthContextModel.READ_RIGHTS +import com.egm.stellio.shared.util.AuthContextModel.SpecificAccessPolicy +import com.egm.stellio.shared.util.AuthContextModel.USER_PREFIX +import com.egm.stellio.shared.util.AuthContextModel.WRITE_RIGHTS import com.egm.stellio.shared.util.CREATION_ROLES import com.egm.stellio.shared.util.GlobalRole import com.egm.stellio.shared.util.toUri @@ -42,7 +42,7 @@ class Neo4jAuthorizationService( // remove the already authorized entities from the list to avoid double-checking them val grantedEntities = filterEntitiesUserHaveOneOfGivenRights( entitiesId.minus(authorizedBySpecificPolicyEntities.toSet()), - READ_RIGHT, + READ_RIGHTS, userSub ) return authorizedBySpecificPolicyEntities.plus(grantedEntities) @@ -54,14 +54,14 @@ class Neo4jAuthorizationService( // remove the already authorized entities from the list to avoid double-checking them val grantedEntities = filterEntitiesUserHaveOneOfGivenRights( entitiesId.minus(authorizedBySpecificPolicyEntities.toSet()), - WRITE_RIGHT, + WRITE_RIGHTS, userSub ) return authorizedBySpecificPolicyEntities.plus(grantedEntities) } override fun filterEntitiesUserCanAdmin(entitiesId: List, userSub: String): List = - filterEntitiesUserHaveOneOfGivenRights(entitiesId, ADMIN_RIGHT, userSub) + filterEntitiesUserHaveOneOfGivenRights(entitiesId, ADMIN_RIGHTS, userSub) override fun splitEntitiesByUserCanAdmin( entitiesId: List, @@ -97,18 +97,18 @@ class Neo4jAuthorizationService( ) override fun userCanReadEntity(entityId: URI, userSub: String): Boolean = - userHasOneOfGivenRightsOnEntity(entityId, READ_RIGHT, userSub) || + userHasOneOfGivenRightsOnEntity(entityId, READ_RIGHTS, userSub) || entityHasSpecificAccessPolicy( entityId, listOf(SpecificAccessPolicy.AUTH_WRITE, SpecificAccessPolicy.AUTH_READ) ) override fun userCanUpdateEntity(entityId: URI, userSub: String): Boolean = - userHasOneOfGivenRightsOnEntity(entityId, WRITE_RIGHT, userSub) || + userHasOneOfGivenRightsOnEntity(entityId, WRITE_RIGHTS, userSub) || entityHasSpecificAccessPolicy(entityId, listOf(SpecificAccessPolicy.AUTH_WRITE)) override fun userIsAdminOfEntity(entityId: URI, userSub: String): Boolean = - userHasOneOfGivenRightsOnEntity(entityId, ADMIN_RIGHT, userSub) + userHasOneOfGivenRightsOnEntity(entityId, ADMIN_RIGHTS, userSub) private fun userHasOneOfGivenRightsOnEntity( entityId: URI, @@ -140,7 +140,7 @@ class Neo4jAuthorizationService( val relationships = entitiesId.map { Relationship( objectId = it, - type = listOf(R_CAN_ADMIN), + type = listOf(AUTH_REL_CAN_ADMIN), datasetId = "urn:ngsi-ld:Dataset:rCanAdmin:$it".toUri() ) } diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/repository/Neo4jRepository.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/repository/Neo4jRepository.kt index 7b7f38ae4..432026bbf 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/repository/Neo4jRepository.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/repository/Neo4jRepository.kt @@ -1,11 +1,11 @@ package com.egm.stellio.entity.repository -import com.egm.stellio.entity.authorization.AuthorizationService import com.egm.stellio.entity.model.Property import com.egm.stellio.entity.model.Relationship import com.egm.stellio.entity.model.toRelationshipTypeName import com.egm.stellio.shared.model.NgsiLdGeoPropertyInstance import com.egm.stellio.shared.model.NgsiLdGeoPropertyInstance.Companion.toWktFormat +import com.egm.stellio.shared.util.AuthContextModel.IAM_TYPES import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LOCATION_PROPERTY import com.egm.stellio.shared.util.toListOfString import com.egm.stellio.shared.util.toUri @@ -482,7 +482,7 @@ class Neo4jRepository( return result.map { rowResult -> val entityWithLocationCount = (rowResult["entityWithLocationCount"] as Long).toInt() val entityTypes = (rowResult["entityType"] as List) - .filter { !authorizationEntitiesTypes.plus("Entity").contains(it) } + .filter { !IAM_TYPES.plus("Entity").contains(it) } entityTypes.map { entityType -> mapOf( "entityType" to entityType, @@ -506,7 +506,7 @@ class Neo4jRepository( val result = neo4jClient.query(query).fetch().all() return result.map { (it["entityType"] as List) - .filter { !authorizationEntitiesTypes.plus("Entity").contains(it) } + .filter { !IAM_TYPES.plus("Entity").contains(it) } }.flatten() } @@ -558,7 +558,7 @@ class Neo4jRepository( } .map { attributeName -> val typeNames = (rowResult["typeNames"] as List) - .filter { !authorizationEntitiesTypes.plus("Entity").contains(it) }.toSet() + .filter { !IAM_TYPES.plus("Entity").contains(it) }.toSet() attributeName to typeNames } }.flatten() @@ -609,7 +609,7 @@ class Neo4jRepository( ), acc.second.plus( (current["typeNames"] as List).filter { - !authorizationEntitiesTypes.plus("Entity").contains(it) + !IAM_TYPES.plus("Entity").contains(it) }.toSet() ), acc.third.plus((current["attributeCount"] as Long).toInt()) @@ -644,10 +644,4 @@ class Neo4jRepository( OPTIONAL MATCH (attribute)-[:HAS_OBJECT]->(relOfAttribute:Relationship) DETACH DELETE attribute, propOfAttribute, relOfAttribute """.trimIndent() - - private val authorizationEntitiesTypes = listOf( - AuthorizationService.AUTHORIZATION_ONTOLOGY + "User", - AuthorizationService.AUTHORIZATION_ONTOLOGY + "Client", - AuthorizationService.AUTHORIZATION_ONTOLOGY + "Group" - ) } diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepository.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepository.kt index ccc378228..7d1aa4208 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepository.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepository.kt @@ -1,8 +1,8 @@ package com.egm.stellio.entity.repository -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.USER_PREFIX import com.egm.stellio.entity.authorization.Neo4jAuthorizationService import com.egm.stellio.shared.model.QueryParams +import com.egm.stellio.shared.util.AuthContextModel.USER_PREFIX import com.egm.stellio.shared.util.toUri import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.data.neo4j.core.Neo4jClient diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/repository/QueryUtils.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/repository/QueryUtils.kt index d2afd580b..027711272 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/repository/QueryUtils.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/repository/QueryUtils.kt @@ -1,6 +1,5 @@ package com.egm.stellio.entity.repository -import com.egm.stellio.entity.authorization.AuthorizationService import com.egm.stellio.entity.util.extractComparisonParametersFromQuery import com.egm.stellio.entity.util.isDate import com.egm.stellio.entity.util.isDateTime @@ -8,6 +7,11 @@ import com.egm.stellio.entity.util.isFloat import com.egm.stellio.entity.util.isRelationshipTarget import com.egm.stellio.entity.util.isTime import com.egm.stellio.shared.model.QueryParams +import com.egm.stellio.shared.util.AuthContextModel +import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_SID +import com.egm.stellio.shared.util.AuthContextModel.CLIENT_TYPE +import com.egm.stellio.shared.util.AuthContextModel.READ_RIGHTS +import com.egm.stellio.shared.util.AuthContextModel.USER_TYPE import com.egm.stellio.shared.util.JsonLdUtils.EGM_SPECIFIC_ACCESS_POLICY import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdKey import java.util.regex.Pattern @@ -29,12 +33,12 @@ object QueryUtils { val matchUserClause = """ CALL { - MATCH (userEntity:Entity:`${AuthorizationService.USER_LABEL}`) + MATCH (userEntity:Entity:`$USER_TYPE`) WHERE userEntity.id = ${'$'}userId RETURN userEntity UNION - MATCH (userEntity:Entity:`${AuthorizationService.CLIENT_LABEL}`) + MATCH (userEntity:Entity:`$CLIENT_TYPE`) WHERE (userEntity)-[:HAS_VALUE] - ->(:Property { name: "${AuthorizationService.SERVICE_ACCOUNT_ID}", value: ${'$'}userId}) + ->(:Property { name: "$AUTH_PROP_SID", value: ${'$'}userId}) RETURN userEntity } with userEntity @@ -52,7 +56,7 @@ object QueryUtils { val matchEntitiesClause = """ MATCH (userEntity)-[:HAS_OBJECT]->(right:Attribute:Relationship)-[]->$matchEntityClause - WHERE any(r IN labels(right) WHERE r IN ${AuthorizationService.READ_RIGHT.map { "'$it'" }}) + WHERE any(r IN labels(right) WHERE r IN ${READ_RIGHTS.map { "'$it'" }}) $finalFilterClause return entity.id as entityId """.trimIndent() @@ -61,7 +65,7 @@ object QueryUtils { """ MATCH (userEntity)-[:HAS_OBJECT]->(:Attribute:Relationship) -[:isMemberOf]->(:Entity)-[:HAS_OBJECT]-(grpRight:Attribute:Relationship)-[]->$matchEntityClause - WHERE any(r IN labels(grpRight) WHERE r IN ${AuthorizationService.READ_RIGHT.map { "'$it'" }}) + WHERE any(r IN labels(grpRight) WHERE r IN ${READ_RIGHTS.map { "'$it'" }}) $finalFilterClause return entity.id as entityId """.trimIndent() @@ -70,8 +74,8 @@ object QueryUtils { """ MATCH $matchEntityClause-[:HAS_VALUE]->(prop:Property { name: "$EGM_SPECIFIC_ACCESS_POLICY" }) WHERE prop.value IN [ - '${AuthorizationService.SpecificAccessPolicy.AUTH_WRITE.name}', - '${AuthorizationService.SpecificAccessPolicy.AUTH_READ.name}' + '${AuthContextModel.SpecificAccessPolicy.AUTH_WRITE.name}', + '${AuthContextModel.SpecificAccessPolicy.AUTH_READ.name}' ] $finalFilterClause return entity.id as entityId diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/service/EntityEventService.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/service/EntityEventService.kt index ad4fa8253..cfd64bb7b 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/service/EntityEventService.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/service/EntityEventService.kt @@ -3,7 +3,6 @@ package com.egm.stellio.entity.service import arrow.core.Validated import arrow.core.invalid import arrow.core.valid -import com.egm.stellio.entity.authorization.AuthorizationService import com.egm.stellio.entity.model.UpdateOperationResult import com.egm.stellio.entity.model.UpdateResult import com.egm.stellio.entity.model.UpdatedDetails @@ -17,6 +16,7 @@ import com.egm.stellio.shared.model.EntityDeleteEvent import com.egm.stellio.shared.model.EntityEvent import com.egm.stellio.shared.model.EntityReplaceEvent import com.egm.stellio.shared.model.ExpandedTerm +import com.egm.stellio.shared.util.AuthContextModel.IAM_TYPES import com.egm.stellio.shared.util.JsonLdUtils import com.egm.stellio.shared.util.JsonLdUtils.compactAndSerialize import com.egm.stellio.shared.util.JsonLdUtils.compactFragment @@ -64,7 +64,7 @@ class EntityEventService( ) private fun entityChannelName(entityType: String) = - if (AuthorizationService.IAM_LABELS.contains(entityType)) + if (IAM_TYPES.contains(entityType)) "cim.iam.rights" else "cim.entity.$entityType" diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt index 32b5dfd96..36f215b0e 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt @@ -1,12 +1,12 @@ package com.egm.stellio.entity.util -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.AUTHZ_PROP_ROLES -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.AUTHZ_PROP_USERNAME -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.USER_LABEL -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.USER_PREFIX import com.egm.stellio.entity.model.Entity import com.egm.stellio.entity.model.Property import com.egm.stellio.entity.repository.EntityRepository +import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_ROLES +import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_USERNAME +import com.egm.stellio.shared.util.AuthContextModel.USER_PREFIX +import com.egm.stellio.shared.util.AuthContextModel.USER_TYPE import com.egm.stellio.shared.util.GlobalRole import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CORE_CONTEXT import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_EGM_AUTHORIZATION_CONTEXT @@ -32,18 +32,18 @@ class ApiTestsBootstrapper( if (apiTestsUser == null) { val entity = Entity( id = ngsiLdUserId, - type = listOf(USER_LABEL), + type = listOf(USER_TYPE), contexts = listOf( NGSILD_EGM_AUTHORIZATION_CONTEXT, NGSILD_CORE_CONTEXT ), properties = mutableListOf( Property( - name = AUTHZ_PROP_ROLES, + name = AUTH_PROP_ROLES, value = listOf(GlobalRole.STELLIO_CREATOR.key) ), Property( - name = AUTHZ_PROP_USERNAME, + name = AUTH_PROP_USERNAME, value = "API Tests" ) ) diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt index bc264592c..eeff5c681 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandler.kt @@ -8,6 +8,7 @@ import com.egm.stellio.entity.service.EntityService import com.egm.stellio.shared.model.NgsiLdRelationship import com.egm.stellio.shared.model.ResourceNotFoundException import com.egm.stellio.shared.model.parseToNgsiLdAttributes +import com.egm.stellio.shared.util.AuthContextModel.ALL_IAM_RIGHTS import com.egm.stellio.shared.util.JSON_LD_CONTENT_TYPE import com.egm.stellio.shared.util.JsonLdUtils import com.egm.stellio.shared.util.checkAndGetContext @@ -51,7 +52,7 @@ class EntityAccessControlHandler( // ensure payload contains only relationships and that they are of a known type val (validAttributes, invalidAttributes) = ngsiLdAttributes.partition { it is NgsiLdRelationship && - AuthorizationService.IAM_RIGHTS.contains(it.name) + ALL_IAM_RIGHTS.contains(it.name) } val invalidAttributesDetails = invalidAttributes.map { NotUpdatedDetails(it.compactName, "Not a relationship or not an authorized relationship name") @@ -129,6 +130,6 @@ class EntityAccessControlHandler( return if (removeResult != 0) ResponseEntity.status(HttpStatus.NO_CONTENT).build() else - throw ResourceNotFoundException("No right found for subject $subjectId on entity $entityId") + throw ResourceNotFoundException("No right found for $subjectId on $entityId") } } diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt index 05885c90d..4bb51ee74 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt @@ -1,16 +1,18 @@ package com.egm.stellio.entity.web import com.egm.stellio.entity.authorization.AuthorizationService -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_ADMIN -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_READ -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_WRITE -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.toCompactTerm import com.egm.stellio.entity.service.EntityService import com.egm.stellio.shared.model.AttributeAppendEvent import com.egm.stellio.shared.model.EntityCreateEvent import com.egm.stellio.shared.model.ExpandedTerm import com.egm.stellio.shared.model.JsonLdEntity import com.egm.stellio.shared.model.QueryParams +import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_ADMIN +import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_READ +import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_WRITE +import com.egm.stellio.shared.util.AuthContextModel.CLIENT_TYPE +import com.egm.stellio.shared.util.AuthContextModel.GROUP_TYPE +import com.egm.stellio.shared.util.AuthContextModel.USER_TYPE import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_ID import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CORE_CONTEXT import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DATASET_ID_PROPERTY @@ -19,6 +21,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.compactAndSerialize import com.egm.stellio.shared.util.JsonLdUtils.compactFragment import com.egm.stellio.shared.util.JsonUtils.serializeObject import com.egm.stellio.shared.util.extractSubjectOrEmpty +import com.egm.stellio.shared.util.toCompactTerm import com.egm.stellio.shared.util.toUri import kotlinx.coroutines.reactive.awaitFirst import org.slf4j.LoggerFactory @@ -47,7 +50,7 @@ class IAMSynchronizer( .body("User is not authorized to sync user referential") val authorizationContexts = listOf(NGSILD_EGM_AUTHORIZATION_CONTEXT, NGSILD_CORE_CONTEXT) - listOf(AuthorizationService.USER_LABEL, AuthorizationService.GROUP_LABEL, AuthorizationService.CLIENT_LABEL) + listOf(USER_TYPE, GROUP_TYPE, CLIENT_TYPE) .asSequence() .map { // do a first search without asking for a result in order to get the total count @@ -73,14 +76,16 @@ class IAMSynchronizer( .map { jsonLdEntity -> // generate an attribute append event per rCanXXX relationship val entitiesRightsEvents = - generateAttributeAppendEvents(jsonLdEntity, R_CAN_ADMIN, authorizationContexts) - .plus(generateAttributeAppendEvents(jsonLdEntity, R_CAN_WRITE, authorizationContexts)) - .plus(generateAttributeAppendEvents(jsonLdEntity, R_CAN_READ, authorizationContexts)) + generateAttributeAppendEvents(jsonLdEntity, AUTH_REL_CAN_ADMIN, authorizationContexts) + .plus(generateAttributeAppendEvents(jsonLdEntity, AUTH_REL_CAN_WRITE, authorizationContexts)) + .plus(generateAttributeAppendEvents(jsonLdEntity, AUTH_REL_CAN_READ, authorizationContexts)) // remove the rCanXXX relationships as they are sent separately val updatedEntity = compactAndSerialize( jsonLdEntity.copy( - properties = jsonLdEntity.properties.minus(listOf(R_CAN_ADMIN, R_CAN_WRITE, R_CAN_READ)), + properties = jsonLdEntity.properties.minus( + listOf(AUTH_REL_CAN_ADMIN, AUTH_REL_CAN_WRITE, AUTH_REL_CAN_READ) + ), ), authorizationContexts, MediaType.APPLICATION_JSON diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt index e3c9e107c..39ff81af4 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt @@ -1,14 +1,5 @@ package com.egm.stellio.entity.authorization -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.AUTHZ_PROP_ROLES -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.CLIENT_LABEL -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_ADMIN -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_READ -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_WRITE -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_IS_MEMBER_OF -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.SERVICE_ACCOUNT_ID -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.USER_LABEL -import com.egm.stellio.entity.authorization.AuthorizationService.SpecificAccessPolicy import com.egm.stellio.entity.config.WithNeo4jContainer import com.egm.stellio.entity.model.Entity import com.egm.stellio.entity.model.Property @@ -18,6 +9,15 @@ import com.egm.stellio.entity.repository.EntitySubjectNode import com.egm.stellio.entity.repository.Neo4jRepository import com.egm.stellio.entity.repository.SubjectNodeInfo import com.egm.stellio.shared.support.WithKafkaContainer +import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_ROLES +import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_SID +import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_ADMIN +import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_READ +import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_WRITE +import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_IS_MEMBER_OF +import com.egm.stellio.shared.util.AuthContextModel.CLIENT_TYPE +import com.egm.stellio.shared.util.AuthContextModel.SpecificAccessPolicy +import com.egm.stellio.shared.util.AuthContextModel.USER_TYPE import com.egm.stellio.shared.util.JsonLdUtils.EGM_SPECIFIC_ACCESS_POLICY import com.egm.stellio.shared.util.toUri import org.junit.jupiter.api.AfterEach @@ -55,16 +55,16 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer @Test fun `it should filter entities authorized for user with given rights`() { - val userEntity = createEntity(userUri, listOf(USER_LABEL), mutableListOf()) + val userEntity = createEntity(userUri, listOf(USER_TYPE), mutableListOf()) val apiaryEntity = createEntity(apiaryUri, listOf("Apiary"), mutableListOf()) - createRelationship(EntitySubjectNode(userEntity.id), R_CAN_READ, apiaryEntity.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_CAN_READ, apiaryEntity.id) val availableRightsForEntities = neo4jAuthorizationRepository.filterEntitiesUserHasOneOfGivenRights( userUri, listOf(apiaryUri), - setOf(R_CAN_READ, R_CAN_WRITE) + setOf(AUTH_REL_CAN_READ, AUTH_REL_CAN_WRITE) ) assertEquals(listOf(apiaryUri), availableRightsForEntities) @@ -72,16 +72,16 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer @Test fun `it should find no entities are authorized by user`() { - val userEntity = createEntity(userUri, listOf(USER_LABEL), mutableListOf()) + val userEntity = createEntity(userUri, listOf(USER_TYPE), mutableListOf()) val apiaryEntity = createEntity(apiaryUri, listOf("Apiary"), mutableListOf()) - createRelationship(EntitySubjectNode(userEntity.id), R_CAN_WRITE, apiaryEntity.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_CAN_WRITE, apiaryEntity.id) val availableRightsForEntities = neo4jAuthorizationRepository.filterEntitiesUserHasOneOfGivenRights( userUri, listOf(apiaryUri), - setOf(R_CAN_READ) + setOf(AUTH_REL_CAN_READ) ) assert(availableRightsForEntities.isEmpty()) @@ -89,22 +89,22 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer @Test fun `it should filter entities that are authorized by user's group`() { - val userEntity = createEntity(userUri, listOf(USER_LABEL), mutableListOf()) + val userEntity = createEntity(userUri, listOf(USER_TYPE), mutableListOf()) val groupEntity = createEntity(groupUri, listOf("Group"), mutableListOf()) - createRelationship(EntitySubjectNode(userEntity.id), R_IS_MEMBER_OF, groupEntity.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_IS_MEMBER_OF, groupEntity.id) val apiaryEntity = createEntity(apiaryUri, listOf("Apiary"), mutableListOf()) val apiaryEntity2 = createEntity(apiary02Uri, listOf("Apiary"), mutableListOf()) - createRelationship(EntitySubjectNode(userEntity.id), R_CAN_WRITE, apiaryEntity.id) - createRelationship(EntitySubjectNode(groupEntity.id), R_CAN_READ, apiaryEntity2.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_CAN_WRITE, apiaryEntity.id) + createRelationship(EntitySubjectNode(groupEntity.id), AUTH_REL_CAN_READ, apiaryEntity2.id) val authorizedEntitiesId = neo4jAuthorizationRepository.filterEntitiesUserHasOneOfGivenRights( userUri, listOf(apiaryUri, apiary02Uri), - setOf(R_CAN_READ, R_CAN_WRITE) + setOf(AUTH_REL_CAN_READ, AUTH_REL_CAN_WRITE) ) assertEquals(listOf(apiaryUri, apiary02Uri), authorizedEntitiesId) @@ -113,23 +113,23 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer @Test fun `it should find no entities are authorized for client`() { val clientEntity = createEntity( - clientUri, listOf(CLIENT_LABEL), + clientUri, listOf(CLIENT_TYPE), mutableListOf( Property( - name = SERVICE_ACCOUNT_ID, + name = AUTH_PROP_SID, value = serviceAccountUri ) ) ) val apiaryEntity = createEntity(apiaryUri, listOf("Apiary"), mutableListOf()) - createRelationship(EntitySubjectNode(clientEntity.id), R_CAN_WRITE, apiaryEntity.id) + createRelationship(EntitySubjectNode(clientEntity.id), AUTH_REL_CAN_WRITE, apiaryEntity.id) val availableRightsForEntities = neo4jAuthorizationRepository.filterEntitiesUserHasOneOfGivenRights( serviceAccountUri, listOf(apiaryUri), - setOf(R_CAN_READ) + setOf(AUTH_REL_CAN_READ) ) assert(availableRightsForEntities.isEmpty()) @@ -138,23 +138,23 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer @Test fun `it should filter entities authorized for client with given rights`() { val clientEntity = createEntity( - clientUri, listOf(CLIENT_LABEL), + clientUri, listOf(CLIENT_TYPE), mutableListOf( Property( - name = SERVICE_ACCOUNT_ID, + name = AUTH_PROP_SID, value = serviceAccountUri ) ) ) val apiaryEntity = createEntity(apiaryUri, listOf("Apiary"), mutableListOf()) - createRelationship(EntitySubjectNode(clientEntity.id), R_CAN_READ, apiaryEntity.id) + createRelationship(EntitySubjectNode(clientEntity.id), AUTH_REL_CAN_READ, apiaryEntity.id) val availableRightsForEntities = neo4jAuthorizationRepository.filterEntitiesUserHasOneOfGivenRights( serviceAccountUri, listOf(apiaryUri), - setOf(R_CAN_READ, R_CAN_WRITE) + setOf(AUTH_REL_CAN_READ, AUTH_REL_CAN_WRITE) ) assertEquals(listOf(apiaryUri), availableRightsForEntities) @@ -237,10 +237,10 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer fun `it should get all user's roles`() { createEntity( userUri, - listOf(USER_LABEL), + listOf(USER_TYPE), mutableListOf( Property( - name = AUTHZ_PROP_ROLES, + name = AUTH_PROP_ROLES, value = listOf("admin", "creator") ) ) @@ -255,14 +255,14 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer fun `it should get all client's roles`() { createEntity( clientUri, - listOf(CLIENT_LABEL), + listOf(CLIENT_TYPE), mutableListOf( Property( - name = AUTHZ_PROP_ROLES, + name = AUTH_PROP_ROLES, value = listOf("admin", "creator") ), Property( - name = SERVICE_ACCOUNT_ID, + name = AUTH_PROP_SID, value = serviceAccountUri ) ) @@ -277,10 +277,10 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer fun `it should get a user's single role`() { createEntity( userUri, - listOf(USER_LABEL), + listOf(USER_TYPE), mutableListOf( Property( - name = AUTHZ_PROP_ROLES, + name = AUTH_PROP_ROLES, value = "admin" ) ) @@ -293,20 +293,20 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer @Test fun `it should get all user's roles from group`() { - val userEntity = createEntity(userUri, listOf(USER_LABEL), mutableListOf()) + val userEntity = createEntity(userUri, listOf(USER_TYPE), mutableListOf()) val groupEntity = createEntity( groupUri, listOf("Group"), mutableListOf( Property( - name = AUTHZ_PROP_ROLES, + name = AUTH_PROP_ROLES, value = listOf("admin") ) ) ) - createRelationship(EntitySubjectNode(userEntity.id), R_IS_MEMBER_OF, groupEntity.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_IS_MEMBER_OF, groupEntity.id) val roles = neo4jAuthorizationRepository.getUserRoles(userUri) @@ -317,10 +317,10 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer fun `it should get all user's roles from user and group`() { val userEntity = createEntity( userUri, - listOf(USER_LABEL), + listOf(USER_TYPE), mutableListOf( Property( - name = AUTHZ_PROP_ROLES, + name = AUTH_PROP_ROLES, value = "admin" ) ) @@ -331,13 +331,13 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer listOf("Group"), mutableListOf( Property( - name = AUTHZ_PROP_ROLES, + name = AUTH_PROP_ROLES, value = listOf("creator") ) ) ) - createRelationship(EntitySubjectNode(userEntity.id), R_IS_MEMBER_OF, groupEntity.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_IS_MEMBER_OF, groupEntity.id) val roles = neo4jAuthorizationRepository.getUserRoles(userUri) @@ -346,20 +346,20 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer @Test fun `it should get a user's single role from group`() { - val userEntity = createEntity(userUri, listOf(USER_LABEL), mutableListOf()) + val userEntity = createEntity(userUri, listOf(USER_TYPE), mutableListOf()) val groupEntity = createEntity( groupUri, listOf("Group"), mutableListOf( Property( - name = AUTHZ_PROP_ROLES, + name = AUTH_PROP_ROLES, value = "admin" ) ) ) - createRelationship(EntitySubjectNode(userEntity.id), R_IS_MEMBER_OF, groupEntity.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_IS_MEMBER_OF, groupEntity.id) val roles = neo4jAuthorizationRepository.getUserRoles(userUri) @@ -368,7 +368,7 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer @Test fun `it should find no user roles`() { - createEntity(userUri, listOf(USER_LABEL), mutableListOf()) + createEntity(userUri, listOf(USER_TYPE), mutableListOf()) val roles = neo4jAuthorizationRepository.getUserRoles(userUri) @@ -379,10 +379,10 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer fun `it should find no client roles`() { createEntity( clientUri, - listOf(CLIENT_LABEL), + listOf(CLIENT_TYPE), mutableListOf( Property( - name = SERVICE_ACCOUNT_ID, + name = AUTH_PROP_SID, value = "some-uuid" ) ) @@ -397,14 +397,14 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer fun `it should find no client roles if the service account is not known`() { createEntity( clientUri, - listOf(CLIENT_LABEL), + listOf(CLIENT_TYPE), mutableListOf( Property( - name = SERVICE_ACCOUNT_ID, + name = AUTH_PROP_SID, value = "some-uuid" ), Property( - name = AUTHZ_PROP_ROLES, + name = AUTH_PROP_ROLES, value = listOf("admin", "creator") ) ) @@ -417,7 +417,7 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer @Test fun `it should create admin links to entities`() { - createEntity(userUri, listOf(USER_LABEL), mutableListOf()) + createEntity(userUri, listOf(USER_TYPE), mutableListOf()) createEntity(apiaryUri, listOf("Apiary"), mutableListOf()) createEntity(apiary02Uri, listOf("Apiary"), mutableListOf()) @@ -428,7 +428,7 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer targetIds.map { Relationship( objectId = it, - type = listOf(R_CAN_ADMIN), + type = listOf(AUTH_REL_CAN_ADMIN), datasetId = "urn:ngsi-ld:Dataset:rCanAdmin:$it".toUri() ) }, @@ -442,10 +442,10 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer fun `it should create admin links to entities for a client`() { createEntity( clientUri, - listOf(CLIENT_LABEL), + listOf(CLIENT_TYPE), mutableListOf( Property( - name = SERVICE_ACCOUNT_ID, + name = AUTH_PROP_SID, value = serviceAccountUri ) ) @@ -460,7 +460,7 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer targetIds.map { Relationship( objectId = it, - type = listOf(R_CAN_ADMIN), + type = listOf(AUTH_REL_CAN_ADMIN), datasetId = "urn:ngsi-ld:Dataset:rCanAdmin:$it".toUri() ) }, @@ -472,9 +472,9 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer @Test fun `it should remove an user's rights on an entity`() { - val userEntity = createEntity(userUri, listOf(USER_LABEL), mutableListOf()) + val userEntity = createEntity(userUri, listOf(USER_TYPE), mutableListOf()) val targetEntity = createEntity(apiaryUri, listOf("Apiary"), mutableListOf()) - createRelationship(EntitySubjectNode(userEntity.id), R_CAN_READ, targetEntity.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_CAN_READ, targetEntity.id) val result = neo4jAuthorizationRepository.removeUserRightsOnEntity(userEntity.id, targetEntity.id) diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationServiceTest.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationServiceTest.kt index b5b1ef0c1..46cfbc050 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationServiceTest.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationServiceTest.kt @@ -1,8 +1,9 @@ package com.egm.stellio.entity.authorization -import com.egm.stellio.entity.authorization.AuthorizationService.* -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.READ_RIGHT -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.WRITE_RIGHT +import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_ADMIN +import com.egm.stellio.shared.util.AuthContextModel.READ_RIGHTS +import com.egm.stellio.shared.util.AuthContextModel.SpecificAccessPolicy +import com.egm.stellio.shared.util.AuthContextModel.WRITE_RIGHTS import com.egm.stellio.shared.util.GlobalRole import com.egm.stellio.shared.util.toListOfUri import com.egm.stellio.shared.util.toUri @@ -128,7 +129,7 @@ class Neo4jAuthorizationServiceTest { neo4jAuthorizationRepository.filterEntitiesUserHasOneOfGivenRights( mockUserUri, entitiesId, - READ_RIGHT + READ_RIGHTS ) } returns listOf("urn:ngsi-ld:Entity:1", "urn:ngsi-ld:Entity:3", "urn:ngsi-ld:Entity:4").toListOfUri() @@ -153,7 +154,7 @@ class Neo4jAuthorizationServiceTest { neo4jAuthorizationRepository.filterEntitiesUserHasOneOfGivenRights( mockUserUri, listOf("urn:ngsi-ld:Entity:2", "urn:ngsi-ld:Entity:3", "urn:ngsi-ld:Entity:5").toListOfUri(), - READ_RIGHT + READ_RIGHTS ) } returns emptyList() @@ -178,7 +179,7 @@ class Neo4jAuthorizationServiceTest { neo4jAuthorizationRepository.filterEntitiesUserHasOneOfGivenRights( mockUserUri, listOf("urn:ngsi-ld:Entity:2", "urn:ngsi-ld:Entity:3", "urn:ngsi-ld:Entity:5").toListOfUri(), - WRITE_RIGHT + WRITE_RIGHTS ) } returns listOf("urn:ngsi-ld:Entity:3").toListOfUri() @@ -220,7 +221,7 @@ class Neo4jAuthorizationServiceTest { mockUserUri, match { it.size == 1 && - it[0].type == listOf(AuthorizationService.R_CAN_ADMIN) && + it[0].type == listOf(AUTH_REL_CAN_ADMIN) && it[0].datasetId == "urn:ngsi-ld:Dataset:rCanAdmin:$entityUri".toUri() }, listOf(entityUri) diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepositoryTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepositoryTests.kt index e902bf39c..35598fc37 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepositoryTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepositoryTests.kt @@ -1,12 +1,5 @@ package com.egm.stellio.entity.repository -import com.egm.stellio.entity.authorization.AuthorizationService -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.AUTHZ_PROP_ROLES -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_ADMIN -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_READ -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_WRITE -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_IS_MEMBER_OF -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.SERVICE_ACCOUNT_ID import com.egm.stellio.entity.authorization.Neo4jAuthorizationService import com.egm.stellio.entity.config.WithNeo4jContainer import com.egm.stellio.entity.model.Entity @@ -14,14 +7,25 @@ import com.egm.stellio.entity.model.Property import com.egm.stellio.entity.model.Relationship import com.egm.stellio.shared.model.QueryParams import com.egm.stellio.shared.support.WithKafkaContainer +import com.egm.stellio.shared.util.AuthContextModel +import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_ROLES +import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_SID +import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_ADMIN +import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_READ +import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_WRITE +import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_IS_MEMBER_OF +import com.egm.stellio.shared.util.AuthContextModel.CLIENT_TYPE +import com.egm.stellio.shared.util.AuthContextModel.USER_TYPE import com.egm.stellio.shared.util.DEFAULT_CONTEXTS import com.egm.stellio.shared.util.JsonLdUtils import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdKey import com.egm.stellio.shared.util.toUri import com.ninjasquad.springmockk.MockkBean import io.mockk.every -import junit.framework.TestCase.* import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource @@ -65,7 +69,7 @@ class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { @Test fun `it should return matching entities that user can access`() { - val userEntity = createEntity(userUri, listOf(AuthorizationService.USER_LABEL), mutableListOf()) + val userEntity = createEntity(userUri, listOf(USER_TYPE), mutableListOf()) val firstEntity = createEntity( beekeeperUri, listOf("Beekeeper"), @@ -81,9 +85,9 @@ class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { listOf("Beekeeper"), mutableListOf(Property(name = expandedNameProperty, value = "Scalpa")) ) - createRelationship(EntitySubjectNode(userEntity.id), R_CAN_WRITE, firstEntity.id) - createRelationship(EntitySubjectNode(userEntity.id), R_CAN_ADMIN, secondEntity.id) - createRelationship(EntitySubjectNode(userEntity.id), R_CAN_READ, thirdEntity.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_CAN_WRITE, firstEntity.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_CAN_ADMIN, secondEntity.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_CAN_READ, thirdEntity.id) val entities = searchRepository.getEntities( QueryParams(expandedType = "Beekeeper", q = "name==\"Scalpa\""), @@ -98,9 +102,9 @@ class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { @Test fun `it should return matching entities that user can access by it's group`() { - val userEntity = createEntity(userUri, listOf(AuthorizationService.USER_LABEL), mutableListOf()) + val userEntity = createEntity(userUri, listOf(USER_TYPE), mutableListOf()) val groupEntity = createEntity(groupUri, listOf("Group"), mutableListOf()) - createRelationship(EntitySubjectNode(userEntity.id), R_IS_MEMBER_OF, groupEntity.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_IS_MEMBER_OF, groupEntity.id) val firstEntity = createEntity( beekeeperUri, listOf("Beekeeper"), @@ -111,8 +115,8 @@ class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { listOf("Beekeeper"), mutableListOf(Property(name = expandedNameProperty, value = "Scalpa")) ) - createRelationship(EntitySubjectNode(groupEntity.id), R_CAN_WRITE, firstEntity.id) - createRelationship(EntitySubjectNode(groupEntity.id), R_CAN_WRITE, secondEntity.id) + createRelationship(EntitySubjectNode(groupEntity.id), AUTH_REL_CAN_WRITE, firstEntity.id) + createRelationship(EntitySubjectNode(groupEntity.id), AUTH_REL_CAN_WRITE, secondEntity.id) val entities = searchRepository.getEntities( QueryParams(expandedType = "Beekeeper", q = "name==\"Scalpa\""), @@ -127,7 +131,7 @@ class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { @Test fun `it should not return a matching entity that user cannot access`() { - createEntity(userUri, listOf(AuthorizationService.USER_LABEL), mutableListOf()) + createEntity(userUri, listOf(USER_TYPE), mutableListOf()) val entity = createEntity( beekeeperUri, listOf("Beekeeper"), @@ -148,10 +152,10 @@ class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { fun `it should return matching entities that client can access`() { val clientEntity = createEntity( clientUri, - listOf(AuthorizationService.CLIENT_LABEL), + listOf(CLIENT_TYPE), mutableListOf( Property( - name = SERVICE_ACCOUNT_ID, + name = AUTH_PROP_SID, value = serviceAccountUri ) ) @@ -166,8 +170,8 @@ class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { listOf("Beekeeper"), mutableListOf(Property(name = expandedNameProperty, value = "Scalpa")) ) - createRelationship(EntitySubjectNode(clientEntity.id), R_CAN_READ, firstEntity.id) - createRelationship(EntitySubjectNode(clientEntity.id), R_CAN_READ, secondEntity.id) + createRelationship(EntitySubjectNode(clientEntity.id), AUTH_REL_CAN_READ, firstEntity.id) + createRelationship(EntitySubjectNode(clientEntity.id), AUTH_REL_CAN_READ, secondEntity.id) val queryParams = QueryParams(expandedType = "Beekeeper", q = "name==\"Scalpa\"") var entities = searchRepository.getEntities( @@ -195,10 +199,10 @@ class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { fun `it should return all matching entities for admin users`() { createEntity( userUri, - listOf(AuthorizationService.USER_LABEL), + listOf(USER_TYPE), mutableListOf( Property( - name = AUTHZ_PROP_ROLES, + name = AUTH_PROP_ROLES, value = "admin" ) ) @@ -229,7 +233,7 @@ class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { @Test fun `it should return matching entities as the specific access policy`() { - createEntity(userUri, listOf(AuthorizationService.USER_LABEL), mutableListOf()) + createEntity(userUri, listOf(USER_TYPE), mutableListOf()) val firstEntity = createEntity( beekeeperUri, listOf("Beekeeper"), @@ -237,7 +241,7 @@ class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { Property(name = expandedNameProperty, value = "Scalpa"), Property( name = JsonLdUtils.EGM_SPECIFIC_ACCESS_POLICY, - value = AuthorizationService.SpecificAccessPolicy.AUTH_READ.name + value = AuthContextModel.SpecificAccessPolicy.AUTH_READ.name ) ) ) @@ -261,7 +265,7 @@ class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { @Test fun `it should return matching entities count`() { - val userEntity = createEntity(userUri, listOf(AuthorizationService.USER_LABEL), mutableListOf()) + val userEntity = createEntity(userUri, listOf(USER_TYPE), mutableListOf()) val firstEntity = createEntity( "urn:ngsi-ld:Beekeeper:01231".toUri(), listOf("Beekeeper"), @@ -277,9 +281,9 @@ class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { listOf("Beekeeper"), mutableListOf(Property(name = expandedNameProperty, value = "Scalpa3")) ) - createRelationship(EntitySubjectNode(userEntity.id), R_CAN_WRITE, firstEntity.id) - createRelationship(EntitySubjectNode(userEntity.id), R_CAN_WRITE, secondEntity.id) - createRelationship(EntitySubjectNode(userEntity.id), R_CAN_READ, thirdEntity.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_CAN_WRITE, firstEntity.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_CAN_WRITE, secondEntity.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_CAN_READ, thirdEntity.id) val entitiesCount = searchRepository.getEntities( QueryParams(expandedType = "Beekeeper", idPattern = "^urn:ngsi-ld:Beekeeper:0.*2$"), @@ -289,12 +293,12 @@ class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { DEFAULT_CONTEXTS ).first - assertEquals(entitiesCount, 2) + Assertions.assertEquals(entitiesCount, 2) } @Test fun `it should return matching entities count when only the count is requested`() { - val userEntity = createEntity(userUri, listOf(AuthorizationService.USER_LABEL), mutableListOf()) + val userEntity = createEntity(userUri, listOf(USER_TYPE), mutableListOf()) val firstEntity = createEntity( "urn:ngsi-ld:Beekeeper:01231".toUri(), listOf("Beekeeper"), @@ -305,8 +309,8 @@ class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { listOf("Beekeeper"), mutableListOf(Property(name = expandedNameProperty, value = "Scalpa2")) ) - createRelationship(EntitySubjectNode(userEntity.id), R_CAN_WRITE, firstEntity.id) - createRelationship(EntitySubjectNode(userEntity.id), R_CAN_WRITE, secondEntity.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_CAN_WRITE, firstEntity.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_CAN_WRITE, secondEntity.id) val countAndEntities = searchRepository.getEntities( QueryParams(expandedType = "Beekeeper", idPattern = "^urn:ngsi-ld:Beekeeper:0.*2$"), @@ -316,8 +320,8 @@ class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { DEFAULT_CONTEXTS ) - assertEquals(countAndEntities.first, 1) - assertEquals(countAndEntities.second, emptyList()) + Assertions.assertEquals(countAndEntities.first, 1) + Assertions.assertEquals(countAndEntities.second, emptyList()) } @ParameterizedTest @@ -328,7 +332,7 @@ class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { limit: Int, expectedEntitiesIds: List ) { - val userEntity = createEntity(userUri, listOf(AuthorizationService.USER_LABEL), mutableListOf()) + val userEntity = createEntity(userUri, listOf(USER_TYPE), mutableListOf()) val firstEntity = createEntity( "urn:ngsi-ld:Beekeeper:01231".toUri(), listOf("Beekeeper"), @@ -344,9 +348,9 @@ class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { listOf("Beekeeper"), mutableListOf(Property(name = expandedNameProperty, value = "Scalpa3")) ) - createRelationship(EntitySubjectNode(userEntity.id), R_CAN_READ, firstEntity.id) - createRelationship(EntitySubjectNode(userEntity.id), R_CAN_READ, secondEntity.id) - createRelationship(EntitySubjectNode(userEntity.id), R_CAN_READ, thirdEntity.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_CAN_READ, firstEntity.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_CAN_READ, secondEntity.id) + createRelationship(EntitySubjectNode(userEntity.id), AUTH_REL_CAN_READ, thirdEntity.id) val entities = searchRepository.getEntities( QueryParams(expandedType = "Beekeeper", idPattern = idPattern), diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt index 6b3775fe5..f73abca94 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt @@ -1,14 +1,14 @@ package com.egm.stellio.entity.web import com.egm.stellio.entity.authorization.AuthorizationService -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_READ -import com.egm.stellio.entity.authorization.AuthorizationService.Companion.R_CAN_WRITE import com.egm.stellio.entity.config.WebSecurityTestConfig import com.egm.stellio.entity.model.UpdateAttributeResult import com.egm.stellio.entity.model.UpdateOperationResult import com.egm.stellio.entity.service.EntityEventService import com.egm.stellio.entity.service.EntityService import com.egm.stellio.shared.WithMockCustomUser +import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_READ +import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_WRITE import com.egm.stellio.shared.util.JSON_LD_MEDIA_TYPE import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CORE_CONTEXT import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_EGM_AUTHORIZATION_CONTEXT @@ -77,7 +77,7 @@ class EntityAccessControlHandlerTests { every { authorizationService.userIsAdminOfEntity(any(), any()) } returns true every { entityService.appendEntityRelationship(any(), any(), any(), any()) - } returns UpdateAttributeResult(R_CAN_READ, null, UpdateOperationResult.APPENDED) + } returns UpdateAttributeResult(AUTH_REL_CAN_READ, null, UpdateOperationResult.APPENDED) every { entityEventService.publishAttributeAppendEvents(any(), any(), any(), any()) } just Runs webClient.post() @@ -97,7 +97,7 @@ class EntityAccessControlHandlerTests { entityService.appendEntityRelationship( eq(subjectId), match { - it.name == R_CAN_READ && + it.name == AUTH_REL_CAN_READ && it.instances.size == 1 }, match { it.objectId == entityUri1 }, @@ -161,19 +161,19 @@ class EntityAccessControlHandlerTests { verify { entityService.appendEntityRelationship( eq(subjectId), - match { it.name == R_CAN_READ }, + match { it.name == AUTH_REL_CAN_READ }, match { it.objectId == entityUri1 }, eq(false) ) entityService.appendEntityRelationship( eq(subjectId), - match { it.name == R_CAN_READ }, + match { it.name == AUTH_REL_CAN_READ }, match { it.objectId == entityUri2 }, eq(false) ) entityService.appendEntityRelationship( eq(subjectId), - match { it.name == R_CAN_WRITE }, + match { it.name == AUTH_REL_CAN_WRITE }, match { it.objectId == entityUri3 }, eq(false) ) @@ -232,7 +232,7 @@ class EntityAccessControlHandlerTests { verify(exactly = 1) { entityService.appendEntityRelationship( eq(subjectId), - match { it.name == R_CAN_READ }, + match { it.name == AUTH_REL_CAN_READ }, match { it.objectId == entityUri1 }, eq(false) ) @@ -347,7 +347,7 @@ class EntityAccessControlHandlerTests { .expectBody().json( """ { - "detail": "Subject urn:ngsi-ld:User:0123 has no right on entity urn:ngsi-ld:Entity:entityId1", + "detail": "No right found for urn:ngsi-ld:User:0123 on urn:ngsi-ld:Entity:entityId1", "type": "https://uri.etsi.org/ngsi-ld/errors/ResourceNotFound", "title": "The referred resource has not been found" } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt index 5ec75c530..aac7b5b97 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityAccessRightsService.kt @@ -49,7 +49,6 @@ class EntityAccessRightsService( .bind("access_right", accessRight.attributeName) .fetch() .rowsUpdated() - .thenReturn(1) .onErrorResume { logger.error("Error while setting access right on entity: $it") Mono.just(-1) @@ -57,20 +56,18 @@ class EntityAccessRightsService( @Transactional fun removeRoleOnEntity(subjectId: UUID, entityId: URI): Mono = - databaseClient - .sql( - """ - DELETE from entity_access_rights - WHERE subject_id = :subject_id - AND entity_id = :entity_id - """.trimIndent() + r2dbcEntityTemplate.delete(EntityAccessRights::class.java) + .matching( + Query.query( + Criteria.where("subject_id").`is`(subjectId) + .and(Criteria.where("entity_id").`is`(entityId)) + ) ) - .bind("subject_id", subjectId) - .bind("entity_id", entityId) - .fetch() - .rowsUpdated() - .thenReturn(1) - .onErrorReturn(-1) + .all() + .onErrorResume { + logger.error("Error while removing access right on entity: $it") + Mono.just(-1) + } fun canReadEntity(subjectId: UUID, entityId: URI): Mono = checkHasAccessRight(subjectId, entityId, listOf(R_CAN_READ, R_CAN_WRITE, R_CAN_ADMIN)) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt index a423b37c9..8216ef937 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt @@ -9,7 +9,11 @@ import com.egm.stellio.shared.model.EntityCreateEvent import com.egm.stellio.shared.model.EntityDeleteEvent import com.egm.stellio.shared.model.EntityEvent import com.egm.stellio.shared.util.AccessRight +import com.egm.stellio.shared.util.AuthContextModel.AUTH_TERM_IS_MEMBER_OF +import com.egm.stellio.shared.util.AuthContextModel.AUTH_TERM_ROLES +import com.egm.stellio.shared.util.AuthContextModel.AUTH_TERM_SID import com.egm.stellio.shared.util.GlobalRole +import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_VALUE import com.egm.stellio.shared.util.JsonUtils import com.egm.stellio.shared.util.SubjectType import com.egm.stellio.shared.util.extractSubjectUuid @@ -34,7 +38,7 @@ class IAMListener( @KafkaListener(topics = ["cim.iam"], groupId = "search-iam") fun processIam(content: String) { - logger.debug("Received event: $content") + logger.debug("Received IAM event: $content") when (val authorizationEvent = JsonUtils.deserializeAs(content)) { is EntityCreateEvent -> createSubjectReferential(authorizationEvent) is EntityDeleteEvent -> deleteSubjectReferential(authorizationEvent) @@ -47,6 +51,7 @@ class IAMListener( @KafkaListener(topics = ["cim.iam.rights"], groupId = "search-iam-rights") fun processIamRights(content: String) { + logger.debug("Received IAM rights event: $content") when (val authorizationEvent = JsonUtils.deserializeAs(content)) { is AttributeAppendEvent -> addEntityToSubject(authorizationEvent) is AttributeDeleteEvent -> removeEntityFromSubject(authorizationEvent) @@ -56,7 +61,7 @@ class IAMListener( @KafkaListener(topics = ["cim.iam.replay"], groupId = "search-iam-replay") fun processIamReplay(content: String) { - logger.debug("Received event: $content") + logger.debug("Received IAM replay event: $content") when (val authorizationEvent = JsonUtils.deserializeAs(content)) { is EntityCreateEvent -> createFullSubjectReferential(authorizationEvent) is AttributeAppendEvent -> addEntityToSubject(authorizationEvent) @@ -68,8 +73,8 @@ class IAMListener( val operationPayloadNode = jacksonObjectMapper().readTree(entityCreateEvent.operationPayload) val roles = extractRoles(operationPayloadNode) val serviceAccountId = - if (operationPayloadNode.has("serviceAccountId")) - (operationPayloadNode["serviceAccountId"] as ObjectNode)["value"].asText() + if (operationPayloadNode.has(AUTH_TERM_SID)) + (operationPayloadNode[AUTH_TERM_SID] as ObjectNode)[JSONLD_VALUE].asText() else null val groupsMemberships = extractGroupsMemberships(operationPayloadNode) val subjectReferential = SubjectReferential( @@ -102,8 +107,8 @@ class IAMListener( } private fun extractGroupsMemberships(operationPayloadNode: JsonNode): List? = - if (operationPayloadNode.has("isMemberOf")) { - when (val isMemberOf = operationPayloadNode["isMemberOf"]) { + if (operationPayloadNode.has(AUTH_TERM_IS_MEMBER_OF)) { + when (val isMemberOf = operationPayloadNode[AUTH_TERM_IS_MEMBER_OF]) { is ObjectNode -> listOf(isMemberOf["object"].asText().extractSubjectUuid()) is ArrayNode -> isMemberOf.map { @@ -114,8 +119,8 @@ class IAMListener( } else null private fun extractRoles(operationPayloadNode: JsonNode): List? = - if (operationPayloadNode.has("roles")) { - when (val rolesValue = (operationPayloadNode["roles"] as ObjectNode)["value"]) { + if (operationPayloadNode.has(AUTH_TERM_ROLES)) { + when (val rolesValue = (operationPayloadNode[AUTH_TERM_ROLES] as ObjectNode)[JSONLD_VALUE]) { is TextNode -> GlobalRole.forKey(rolesValue.asText()).map { listOf(it) }.orNull() is ArrayNode -> rolesValue.map { @@ -135,21 +140,21 @@ class IAMListener( private fun updateSubjectProfile(attributeAppendEvent: AttributeAppendEvent) { val operationPayloadNode = jacksonObjectMapper().readTree(attributeAppendEvent.operationPayload) val subjectUuid = attributeAppendEvent.entityId.extractSubjectUuid() - if (attributeAppendEvent.attributeName == "roles") { - val newRoles = (operationPayloadNode["value"] as ArrayNode).map { + if (attributeAppendEvent.attributeName == AUTH_TERM_ROLES) { + val newRoles = (operationPayloadNode[JSONLD_VALUE] as ArrayNode).map { GlobalRole.forKey(it.asText()) }.flattenOption() if (newRoles.isNotEmpty()) subjectReferentialService.setGlobalRoles(subjectUuid, newRoles).subscribe() else subjectReferentialService.resetGlobalRoles(subjectUuid).subscribe() - } else if (attributeAppendEvent.attributeName == "serviceAccountId") { - val serviceAccountId = operationPayloadNode["value"].asText() + } else if (attributeAppendEvent.attributeName == AUTH_TERM_SID) { + val serviceAccountId = operationPayloadNode[JSONLD_VALUE].asText() subjectReferentialService.addServiceAccountIdToClient( subjectUuid, serviceAccountId.extractSubjectUuid() ).subscribe() - } else if (attributeAppendEvent.attributeName == "isMemberOf") { + } else if (attributeAppendEvent.attributeName == AUTH_TERM_IS_MEMBER_OF) { val groupId = operationPayloadNode["object"].asText() subjectReferentialService.addGroupMembershipToUser( subjectUuid, @@ -174,13 +179,17 @@ class IAMListener( attributeAppendEvent.entityId.extractSubjectUuid(), entityId.toUri(), AccessRight.forAttributeName(attributeAppendEvent.attributeName) - ).subscribe() + ).subscribe { + logger.debug("Set role on entity returned with result: $it") + } } private fun removeEntityFromSubject(attributeDeleteEvent: AttributeDeleteEvent) { entityAccessRightsService.removeRoleOnEntity( attributeDeleteEvent.entityId.extractSubjectUuid(), attributeDeleteEvent.attributeName.toUri() - ).subscribe() + ).subscribe { + logger.debug("Remove role on entity returned with result: $it") + } } } diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt index 0c7cf1d1f..4e24bcbcf 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt @@ -2,6 +2,7 @@ package com.egm.stellio.shared.util import arrow.core.Option import arrow.core.toOption +import com.egm.stellio.shared.model.ExpandedTerm import com.egm.stellio.shared.util.GlobalRole.STELLIO_ADMIN import com.egm.stellio.shared.util.GlobalRole.STELLIO_CREATOR import org.springframework.security.core.context.ReactiveSecurityContextHolder @@ -14,6 +15,39 @@ import java.util.UUID val ADMIN_ROLES: Set = setOf(STELLIO_ADMIN) val CREATION_ROLES: Set = setOf(STELLIO_CREATOR).plus(ADMIN_ROLES) +object AuthContextModel { + private const val AUTHORIZATION_ONTOLOGY = "https://ontology.eglobalmark.com/authorization#" + + const val USER_PREFIX = "urn:ngsi-ld:User:" + + const val USER_TYPE: ExpandedTerm = AUTHORIZATION_ONTOLOGY + "User" + const val GROUP_TYPE: ExpandedTerm = AUTHORIZATION_ONTOLOGY + "Group" + const val CLIENT_TYPE: ExpandedTerm = AUTHORIZATION_ONTOLOGY + "Client" + val IAM_TYPES = setOf(USER_TYPE, GROUP_TYPE, CLIENT_TYPE) + + const val AUTH_TERM_SID = "serviceAccountId" + const val AUTH_PROP_SID: ExpandedTerm = AUTHORIZATION_ONTOLOGY + AUTH_TERM_SID + const val AUTH_TERM_ROLES = "roles" + const val AUTH_PROP_ROLES: ExpandedTerm = AUTHORIZATION_ONTOLOGY + AUTH_TERM_ROLES + const val AUTH_TERM_USERNAME = "username" + const val AUTH_PROP_USERNAME: ExpandedTerm = AUTHORIZATION_ONTOLOGY + AUTH_TERM_USERNAME + + const val AUTH_TERM_IS_MEMBER_OF = "isMemberOf" + const val AUTH_REL_IS_MEMBER_OF: ExpandedTerm = AUTHORIZATION_ONTOLOGY + AUTH_TERM_IS_MEMBER_OF + const val AUTH_REL_CAN_READ: ExpandedTerm = AUTHORIZATION_ONTOLOGY + "rCanRead" + const val AUTH_REL_CAN_WRITE: ExpandedTerm = AUTHORIZATION_ONTOLOGY + "rCanWrite" + const val AUTH_REL_CAN_ADMIN: ExpandedTerm = AUTHORIZATION_ONTOLOGY + "rCanAdmin" + val ALL_IAM_RIGHTS = setOf(AUTH_REL_CAN_READ, AUTH_REL_CAN_WRITE, AUTH_REL_CAN_ADMIN) + val ADMIN_RIGHTS: Set = setOf(AUTH_REL_CAN_ADMIN) + val WRITE_RIGHTS: Set = setOf(AUTH_REL_CAN_WRITE).plus(ADMIN_RIGHTS) + val READ_RIGHTS: Set = setOf(AUTH_REL_CAN_READ).plus(WRITE_RIGHTS) + + enum class SpecificAccessPolicy { + AUTH_READ, + AUTH_WRITE + } +} + fun extractSubjectOrEmpty(): Mono { return ReactiveSecurityContextHolder.getContext() .switchIfEmpty(Mono.just(SecurityContextImpl())) @@ -21,7 +55,7 @@ fun extractSubjectOrEmpty(): Mono { } fun URI.extractSubjectUuid(): UUID = - UUID.fromString(this.toString().substringAfterLast(":")) + this.toString().extractSubjectUuid() fun String.extractSubjectUuid(): UUID = UUID.fromString(this.substringAfterLast(":")) @@ -29,6 +63,9 @@ fun String.extractSubjectUuid(): UUID = fun String.toUUID(): UUID = UUID.fromString(this) +// specific to authz terms where we know the compacted term is what is after the last # character +fun ExpandedTerm.toCompactTerm(): String = this.substringAfterLast("#") + enum class SubjectType { USER, GROUP, diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt index d55adccdf..640daa1b2 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt @@ -47,6 +47,7 @@ object JsonLdUtils { const val JSONLD_ID = "@id" const val JSONLD_TYPE = "@type" + const val JSONLD_VALUE = "value" const val JSONLD_VALUE_KW = "@value" const val JSONLD_CONTEXT = "@context" val JSONLD_EXPANDED_ENTITY_MANDATORY_FIELDS = setOf(JSONLD_ID, JSONLD_TYPE, JSONLD_CONTEXT) From 002c79e0f233a336a0cf23aad4aa34ee796e6a61 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Mon, 3 Jan 2022 08:05:26 +0100 Subject: [PATCH 27/28] refactor: move security specific constants into AuthUtils --- .../authorization/Neo4jAuthorizationRepository.kt | 4 ++-- .../com/egm/stellio/entity/repository/QueryUtils.kt | 4 ++-- .../egm/stellio/entity/util/ApiTestsBootstrapper.kt | 2 +- .../kotlin/com/egm/stellio/entity/web/EntityHandler.kt | 3 +-- .../com/egm/stellio/entity/web/IAMSynchronizer.kt | 2 +- .../authorization/Neo4jAuthorizationRepositoryTest.kt | 10 +++++----- .../entity/repository/Neo4jSearchRepositoryTests.kt | 4 ++-- .../entity/web/EntityAccessControlHandlerTests.kt | 2 +- .../kotlin/com/egm/stellio/shared/util/AuthUtils.kt | 4 ++++ .../kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt | 2 -- 10 files changed, 19 insertions(+), 18 deletions(-) diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt index 2a7850eee..e21de12e1 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepository.kt @@ -2,11 +2,11 @@ package com.egm.stellio.entity.authorization import com.egm.stellio.entity.model.Relationship import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_ROLES +import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_SAP import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_SID import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_ADMIN import com.egm.stellio.shared.util.AuthContextModel.CLIENT_TYPE import com.egm.stellio.shared.util.AuthContextModel.USER_TYPE -import com.egm.stellio.shared.util.JsonLdUtils.EGM_SPECIFIC_ACCESS_POLICY import com.egm.stellio.shared.util.toListOfString import com.egm.stellio.shared.util.toUri import org.springframework.data.neo4j.core.Neo4jClient @@ -61,7 +61,7 @@ class Neo4jAuthorizationRepository( """ MATCH (entity:Entity) WHERE entity.id IN ${'$'}entitiesId - MATCH (entity)-[:HAS_VALUE]->(p:Property { name: "$EGM_SPECIFIC_ACCESS_POLICY" }) + MATCH (entity)-[:HAS_VALUE]->(p:Property { name: "$AUTH_PROP_SAP" }) WHERE p.value IN ${'$'}specificAccessPolicies RETURN entity.id as id """.trimIndent() diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/repository/QueryUtils.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/repository/QueryUtils.kt index 027711272..321600cfb 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/repository/QueryUtils.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/repository/QueryUtils.kt @@ -8,11 +8,11 @@ import com.egm.stellio.entity.util.isRelationshipTarget import com.egm.stellio.entity.util.isTime import com.egm.stellio.shared.model.QueryParams import com.egm.stellio.shared.util.AuthContextModel +import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_SAP import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_SID import com.egm.stellio.shared.util.AuthContextModel.CLIENT_TYPE import com.egm.stellio.shared.util.AuthContextModel.READ_RIGHTS import com.egm.stellio.shared.util.AuthContextModel.USER_TYPE -import com.egm.stellio.shared.util.JsonLdUtils.EGM_SPECIFIC_ACCESS_POLICY import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdKey import java.util.regex.Pattern @@ -72,7 +72,7 @@ object QueryUtils { val matchAuthorizedPerAccessPolicyClause = """ - MATCH $matchEntityClause-[:HAS_VALUE]->(prop:Property { name: "$EGM_SPECIFIC_ACCESS_POLICY" }) + MATCH $matchEntityClause-[:HAS_VALUE]->(prop:Property { name: "$AUTH_PROP_SAP" }) WHERE prop.value IN [ '${AuthContextModel.SpecificAccessPolicy.AUTH_WRITE.name}', '${AuthContextModel.SpecificAccessPolicy.AUTH_READ.name}' diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt index 36f215b0e..3f51dbe06 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/util/ApiTestsBootstrapper.kt @@ -5,11 +5,11 @@ import com.egm.stellio.entity.model.Property import com.egm.stellio.entity.repository.EntityRepository import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_ROLES import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_USERNAME +import com.egm.stellio.shared.util.AuthContextModel.NGSILD_EGM_AUTHORIZATION_CONTEXT import com.egm.stellio.shared.util.AuthContextModel.USER_PREFIX import com.egm.stellio.shared.util.AuthContextModel.USER_TYPE import com.egm.stellio.shared.util.GlobalRole import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CORE_CONTEXT -import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_EGM_AUTHORIZATION_CONTEXT import com.egm.stellio.shared.util.toUri import org.springframework.beans.factory.annotation.Value import org.springframework.boot.CommandLineRunner diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityHandler.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityHandler.kt index ec42c145a..f67d45e7e 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityHandler.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/EntityHandler.kt @@ -7,7 +7,6 @@ import com.egm.stellio.entity.service.EntityEventService import com.egm.stellio.entity.service.EntityService import com.egm.stellio.shared.model.* import com.egm.stellio.shared.util.* -import com.egm.stellio.shared.util.JsonLdUtils.EGM_SPECIFIC_ACCESS_POLICY import com.egm.stellio.shared.util.JsonLdUtils.compactEntities import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdEntity import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdFragment @@ -423,7 +422,7 @@ class EntityHandler( } private fun checkAttributeIsAuthorized(expandedAttributeName: String, entityUri: URI, userId: String) { - if (expandedAttributeName == EGM_SPECIFIC_ACCESS_POLICY && + if (expandedAttributeName == AuthContextModel.AUTH_PROP_SAP && !authorizationService.userIsAdminOfEntity(entityUri, userId) ) throw AccessDeniedException("User forbidden to update access policy of entity $entityUri") diff --git a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt index 4bb51ee74..4c9c31a6c 100644 --- a/entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt +++ b/entity-service/src/main/kotlin/com/egm/stellio/entity/web/IAMSynchronizer.kt @@ -12,11 +12,11 @@ import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_READ import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_WRITE import com.egm.stellio.shared.util.AuthContextModel.CLIENT_TYPE import com.egm.stellio.shared.util.AuthContextModel.GROUP_TYPE +import com.egm.stellio.shared.util.AuthContextModel.NGSILD_EGM_AUTHORIZATION_CONTEXT import com.egm.stellio.shared.util.AuthContextModel.USER_TYPE import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_ID import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CORE_CONTEXT import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DATASET_ID_PROPERTY -import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_EGM_AUTHORIZATION_CONTEXT import com.egm.stellio.shared.util.JsonLdUtils.compactAndSerialize import com.egm.stellio.shared.util.JsonLdUtils.compactFragment import com.egm.stellio.shared.util.JsonUtils.serializeObject diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt index 39ff81af4..555b6d21e 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/authorization/Neo4jAuthorizationRepositoryTest.kt @@ -9,6 +9,7 @@ import com.egm.stellio.entity.repository.EntitySubjectNode import com.egm.stellio.entity.repository.Neo4jRepository import com.egm.stellio.entity.repository.SubjectNodeInfo import com.egm.stellio.shared.support.WithKafkaContainer +import com.egm.stellio.shared.util.AuthContextModel import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_ROLES import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_SID import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_ADMIN @@ -18,7 +19,6 @@ import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_IS_MEMBER_OF import com.egm.stellio.shared.util.AuthContextModel.CLIENT_TYPE import com.egm.stellio.shared.util.AuthContextModel.SpecificAccessPolicy import com.egm.stellio.shared.util.AuthContextModel.USER_TYPE -import com.egm.stellio.shared.util.JsonLdUtils.EGM_SPECIFIC_ACCESS_POLICY import com.egm.stellio.shared.util.toUri import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals @@ -166,7 +166,7 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer apiaryUri, listOf("Apiary"), mutableListOf( Property( - name = EGM_SPECIFIC_ACCESS_POLICY, + name = AuthContextModel.AUTH_PROP_SAP, value = SpecificAccessPolicy.AUTH_READ.name ) ) @@ -188,7 +188,7 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer apiaryUri, listOf("Apiary"), mutableListOf( Property( - name = EGM_SPECIFIC_ACCESS_POLICY, + name = AuthContextModel.AUTH_PROP_SAP, value = SpecificAccessPolicy.AUTH_WRITE.name ) ) @@ -209,7 +209,7 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer apiaryUri, listOf("Apiary"), mutableListOf( Property( - name = EGM_SPECIFIC_ACCESS_POLICY, + name = AuthContextModel.AUTH_PROP_SAP, value = SpecificAccessPolicy.AUTH_WRITE.name ) ) @@ -218,7 +218,7 @@ class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer apiary02Uri, listOf("Apiary"), mutableListOf( Property( - name = EGM_SPECIFIC_ACCESS_POLICY, + name = AuthContextModel.AUTH_PROP_SAP, value = SpecificAccessPolicy.AUTH_READ.name ) ) diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepositoryTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepositoryTests.kt index 35598fc37..10f6069bc 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepositoryTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/repository/Neo4jSearchRepositoryTests.kt @@ -9,6 +9,7 @@ import com.egm.stellio.shared.model.QueryParams import com.egm.stellio.shared.support.WithKafkaContainer import com.egm.stellio.shared.util.AuthContextModel import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_ROLES +import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_SAP import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_SID import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_ADMIN import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_READ @@ -17,7 +18,6 @@ import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_IS_MEMBER_OF import com.egm.stellio.shared.util.AuthContextModel.CLIENT_TYPE import com.egm.stellio.shared.util.AuthContextModel.USER_TYPE import com.egm.stellio.shared.util.DEFAULT_CONTEXTS -import com.egm.stellio.shared.util.JsonLdUtils import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdKey import com.egm.stellio.shared.util.toUri import com.ninjasquad.springmockk.MockkBean @@ -240,7 +240,7 @@ class Neo4jSearchRepositoryTests : WithNeo4jContainer, WithKafkaContainer { mutableListOf( Property(name = expandedNameProperty, value = "Scalpa"), Property( - name = JsonLdUtils.EGM_SPECIFIC_ACCESS_POLICY, + name = AUTH_PROP_SAP, value = AuthContextModel.SpecificAccessPolicy.AUTH_READ.name ) ) diff --git a/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt index f73abca94..ac64ad08e 100644 --- a/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt +++ b/entity-service/src/test/kotlin/com/egm/stellio/entity/web/EntityAccessControlHandlerTests.kt @@ -9,9 +9,9 @@ import com.egm.stellio.entity.service.EntityService import com.egm.stellio.shared.WithMockCustomUser import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_READ import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_CAN_WRITE +import com.egm.stellio.shared.util.AuthContextModel.NGSILD_EGM_AUTHORIZATION_CONTEXT import com.egm.stellio.shared.util.JSON_LD_MEDIA_TYPE import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CORE_CONTEXT -import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_EGM_AUTHORIZATION_CONTEXT import com.egm.stellio.shared.util.buildContextLinkHeader import com.egm.stellio.shared.util.toUri import com.ninjasquad.springmockk.MockkBean diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt index 4e24bcbcf..0a7a56017 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt @@ -5,6 +5,7 @@ import arrow.core.toOption import com.egm.stellio.shared.model.ExpandedTerm import com.egm.stellio.shared.util.GlobalRole.STELLIO_ADMIN import com.egm.stellio.shared.util.GlobalRole.STELLIO_CREATOR +import com.egm.stellio.shared.util.JsonLdUtils.EGM_BASE_CONTEXT_URL import org.springframework.security.core.context.ReactiveSecurityContextHolder import org.springframework.security.core.context.SecurityContextImpl import org.springframework.security.oauth2.jwt.Jwt @@ -16,6 +17,7 @@ val ADMIN_ROLES: Set = setOf(STELLIO_ADMIN) val CREATION_ROLES: Set = setOf(STELLIO_CREATOR).plus(ADMIN_ROLES) object AuthContextModel { + val NGSILD_EGM_AUTHORIZATION_CONTEXT = "$EGM_BASE_CONTEXT_URL/authorization/jsonld-contexts/authorization.jsonld" private const val AUTHORIZATION_ONTOLOGY = "https://ontology.eglobalmark.com/authorization#" const val USER_PREFIX = "urn:ngsi-ld:User:" @@ -31,6 +33,8 @@ object AuthContextModel { const val AUTH_PROP_ROLES: ExpandedTerm = AUTHORIZATION_ONTOLOGY + AUTH_TERM_ROLES const val AUTH_TERM_USERNAME = "username" const val AUTH_PROP_USERNAME: ExpandedTerm = AUTHORIZATION_ONTOLOGY + AUTH_TERM_USERNAME + const val AUTH_TERM_SAP = "specificAccessPolicy" + const val AUTH_PROP_SAP = AUTHORIZATION_ONTOLOGY + AUTH_TERM_SAP const val AUTH_TERM_IS_MEMBER_OF = "isMemberOf" const val AUTH_REL_IS_MEMBER_OF: ExpandedTerm = AUTHORIZATION_ONTOLOGY + AUTH_TERM_IS_MEMBER_OF diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt index 640daa1b2..296e33dcd 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt @@ -35,7 +35,6 @@ object JsonLdUtils { const val EGM_BASE_CONTEXT_URL = "https://raw.githubusercontent.com/easy-global-market/ngsild-api-data-models/master" val NGSILD_EGM_CONTEXT = "$EGM_BASE_CONTEXT_URL/shared-jsonld-contexts/egm.jsonld" - val NGSILD_EGM_AUTHORIZATION_CONTEXT = "$EGM_BASE_CONTEXT_URL/authorization/jsonld-contexts/authorization.jsonld" val NGSILD_PROPERTY_TYPE = AttributeType("https://uri.etsi.org/ngsi-ld/Property") const val NGSILD_PROPERTY_VALUE = "https://uri.etsi.org/ngsi-ld/hasValue" @@ -68,7 +67,6 @@ object JsonLdUtils { const val EGM_OBSERVED_BY = "https://ontology.eglobalmark.com/egm#observedBy" const val EGM_RAISED_NOTIFICATION = "https://ontology.eglobalmark.com/egm#raised" - const val EGM_SPECIFIC_ACCESS_POLICY = "https://ontology.eglobalmark.com/egm#specificAccessPolicy" val logger = LoggerFactory.getLogger(javaClass) From 6218bf2efd4b6f842b123b31c0017f536fae08d0 Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Mon, 3 Jan 2022 10:26:37 +0100 Subject: [PATCH 28/28] qual(common): add some more tests --- .../egm/stellio/search/service/IAMListener.kt | 30 +++++----- .../service/SubjectReferentialService.kt | 21 +++---- .../service/EntityAccessRightsServiceTests.kt | 11 ++++ .../com/egm/stellio/shared/util/AuthUtils.kt | 6 +- .../egm/stellio/shared/util/AuthUtilsTests.kt | 58 +++++++++++++++++++ 5 files changed, 93 insertions(+), 33 deletions(-) create mode 100644 shared/src/test/kotlin/com/egm/stellio/shared/util/AuthUtilsTests.kt diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt index 8216ef937..4a7911cf4 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/IAMListener.kt @@ -1,5 +1,7 @@ package com.egm.stellio.search.service +import arrow.core.None +import arrow.core.Some import arrow.core.flattenOption import com.egm.stellio.search.model.SubjectReferential import com.egm.stellio.shared.model.AttributeAppendEvent @@ -85,10 +87,7 @@ class IAMListener( groupsMemberships = groupsMemberships ) - subjectReferentialService.create(subjectReferential) - .subscribe { - logger.debug("Created subject ${entityCreateEvent.entityId}") - } + subjectReferentialService.create(subjectReferential).subscribe() } private fun createSubjectReferential(entityCreateEvent: EntityCreateEvent) { @@ -100,10 +99,7 @@ class IAMListener( globalRoles = roles ) - subjectReferentialService.create(subjectReferential) - .subscribe { - logger.debug("Created subject ${entityCreateEvent.entityId}") - } + subjectReferentialService.create(subjectReferential).subscribe() } private fun extractGroupsMemberships(operationPayloadNode: JsonNode): List? = @@ -175,12 +171,14 @@ class IAMListener( private fun addEntityToSubject(attributeAppendEvent: AttributeAppendEvent) { val operationPayloadNode = jacksonObjectMapper().readTree(attributeAppendEvent.operationPayload) val entityId = operationPayloadNode["object"].asText() - entityAccessRightsService.setRoleOnEntity( - attributeAppendEvent.entityId.extractSubjectUuid(), - entityId.toUri(), - AccessRight.forAttributeName(attributeAppendEvent.attributeName) - ).subscribe { - logger.debug("Set role on entity returned with result: $it") + when (val accessRight = AccessRight.forAttributeName(attributeAppendEvent.attributeName)) { + is Some -> + entityAccessRightsService.setRoleOnEntity( + attributeAppendEvent.entityId.extractSubjectUuid(), + entityId.toUri(), + accessRight.value + ).subscribe() + is None -> logger.warn("Unable to extract a known access right from $accessRight") } } @@ -188,8 +186,6 @@ class IAMListener( entityAccessRightsService.removeRoleOnEntity( attributeDeleteEvent.entityId.extractSubjectUuid(), attributeDeleteEvent.attributeName.toUri() - ).subscribe { - logger.debug("Remove role on entity returned with result: $it") - } + ).subscribe() } } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt index ae0d51b19..c89ed0a62 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubjectReferentialService.kt @@ -45,7 +45,6 @@ class SubjectReferentialService( .bind("groups_memberships", subjectReferential.groupsMemberships?.toTypedArray()) .fetch() .rowsUpdated() - .thenReturn(1) .onErrorResume { logger.error("Error while creating a new subject referential : ${it.message}", it) Mono.just(-1) @@ -117,7 +116,6 @@ class SubjectReferentialService( .bind("global_roles", newRoles.map { it.key }.toTypedArray()) .fetch() .rowsUpdated() - .thenReturn(1) .onErrorResume { logger.error("Error while setting global roles: $it") Mono.just(-1) @@ -136,7 +134,6 @@ class SubjectReferentialService( .bind("subject_id", subjectId) .fetch() .rowsUpdated() - .thenReturn(1) .onErrorResume { logger.error("Error while resetting global roles: $it") Mono.just(-1) @@ -147,9 +144,9 @@ class SubjectReferentialService( databaseClient .sql( """ - UPDATE subject_referential - SET groups_memberships = array_append(groups_memberships, :group_id::text) - WHERE (subject_id = :subject_id OR service_account_id = :subject_id) + UPDATE subject_referential + SET groups_memberships = array_append(groups_memberships, :group_id::text) + WHERE (subject_id = :subject_id OR service_account_id = :subject_id) """.trimIndent() ) .bind("subject_id", subjectId) @@ -166,9 +163,9 @@ class SubjectReferentialService( databaseClient .sql( """ - UPDATE subject_referential - SET groups_memberships = array_remove(groups_memberships, :group_id::text) - WHERE (subject_id = :subject_id OR service_account_id = :subject_id) + UPDATE subject_referential + SET groups_memberships = array_remove(groups_memberships, :group_id::text) + WHERE (subject_id = :subject_id OR service_account_id = :subject_id) """.trimIndent() ) .bind("subject_id", subjectId) @@ -185,9 +182,9 @@ class SubjectReferentialService( databaseClient .sql( """ - UPDATE subject_referential - SET service_account_id = :service_account_id - WHERE subject_id = :subject_id + UPDATE subject_referential + SET service_account_id = :service_account_id + WHERE subject_id = :subject_id """.trimIndent() ) .bind("subject_id", subjectId) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityAccessRightsServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityAccessRightsServiceTests.kt index f0e88b8db..2537e6980 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityAccessRightsServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityAccessRightsServiceTests.kt @@ -216,4 +216,15 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer, WithKafkaContaine ) } } + + @Test + fun `it should delete entity access rights associated to an user`() { + entityAccessRightsService.setReadRoleOnEntity(subjectUuid, entityId).block() + + StepVerifier + .create(entityAccessRightsService.delete(subjectUuid)) + .expectNextMatches { it == 1 } + .expectComplete() + .verify() + } } diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt index 0a7a56017..6cc23e219 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt @@ -92,9 +92,7 @@ enum class AccessRight(val attributeName: String) { R_CAN_ADMIN("rCanAdmin"); companion object { - fun forAttributeName(attributeName: String): AccessRight = - values().find { - it.attributeName == attributeName - } ?: throw IllegalArgumentException("Unrecognized attribute name $attributeName") + fun forAttributeName(attributeName: String): Option = + values().find { it.attributeName == attributeName }.toOption() } } diff --git a/shared/src/test/kotlin/com/egm/stellio/shared/util/AuthUtilsTests.kt b/shared/src/test/kotlin/com/egm/stellio/shared/util/AuthUtilsTests.kt new file mode 100644 index 000000000..f89345583 --- /dev/null +++ b/shared/src/test/kotlin/com/egm/stellio/shared/util/AuthUtilsTests.kt @@ -0,0 +1,58 @@ +package com.egm.stellio.shared.util + +import arrow.core.None +import arrow.core.Some +import com.egm.stellio.shared.util.AuthContextModel.AUTH_TERM_SID +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.springframework.test.context.ActiveProfiles +import java.net.URI +import java.util.UUID + +@ActiveProfiles("test") +class AuthUtilsTests { + + @Test + fun `it should extract UUID from an entity URI`() { + assertEquals( + UUID.fromString("3693C62A-D5B2-4F9E-9D3A-F82814984D5C"), + URI.create("urn:ngsi-ld:Entity:3693C62A-D5B2-4F9E-9D3A-F82814984D5C").extractSubjectUuid() + ) + } + + @Test + fun `it should extract UUID from a string version of an entity URI`() { + assertEquals( + UUID.fromString("3693C62A-D5B2-4F9E-9D3A-F82814984D5C"), + "urn:ngsi-ld:Entity:3693C62A-D5B2-4F9E-9D3A-F82814984D5C".extractSubjectUuid() + ) + } + + @Test + fun `it should extract the compact form of an authorization term`() { + assertEquals("serviceAccountId", AUTH_TERM_SID.toCompactTerm()) + } + + @Test + fun `it should find the global role with a given key`() { + assertEquals(Some(GlobalRole.STELLIO_ADMIN), GlobalRole.forKey("stellio-admin")) + assertEquals(Some(GlobalRole.STELLIO_CREATOR), GlobalRole.forKey("stellio-creator")) + } + + @Test + fun `it should not find the global role for an unknown key`() { + assertEquals(None, GlobalRole.forKey("unknown-role")) + } + + @Test + fun `it should find the access right with a given key`() { + assertEquals(Some(AccessRight.R_CAN_READ), AccessRight.forAttributeName("rCanRead")) + assertEquals(Some(AccessRight.R_CAN_WRITE), AccessRight.forAttributeName("rCanWrite")) + assertEquals(Some(AccessRight.R_CAN_ADMIN), AccessRight.forAttributeName("rCanAdmin")) + } + + @Test + fun `it should not find the access right for an unknown key`() { + assertEquals(None, AccessRight.forAttributeName("unknown-access-right")) + } +}