Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: introduce a rights management API #520

Merged
merged 28 commits into from
Jan 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b311524
feat(entity): add endpoints to add / remove rights on entities for an…
bobeal Jun 20, 2021
8583ea9
feat(search): add support for subjects access rights
bobeal Jun 21, 2021
fa2306c
feat(entity): propagate changes of access rights on entities to Kafka
bobeal Jun 23, 2021
c3eb55b
feat(entity): handle case where the right to be removed does not exist
bobeal Jun 23, 2021
c64ae2f
fix(entity): remove attribute name key in operation payloads
bobeal Jun 23, 2021
8bfb10e
feat(search): add listener for events related to access rights on ent…
bobeal Jun 23, 2021
7b211a6
refactor(search): store sub uuid instead of entity URI
bobeal Jun 23, 2021
0b660f2
feat(search): add access control when getting temporal evolution of a…
bobeal Jun 23, 2021
63b69be
feat(api-gateway): add route for access control API
bobeal Jun 24, 2021
9699e8d
migrate code to last version (new SB version, refactored event model,…
bobeal Nov 28, 2021
ba77105
refactor: integrate entity access handler with common event service
bobeal Nov 29, 2021
293b5c1
refactor: put common sample tests payloads in shared testFixtures
bobeal Nov 29, 2021
91a0320
feat(entity): finalize endpoint to add rights
bobeal Dec 1, 2021
0ea1293
feat(entity): review and improve endpoint to remove rights of an user…
bobeal Dec 4, 2021
d06297b
refactor: move some authz related constants to the shared authz file
bobeal Dec 5, 2021
58645e4
feat(search): refactor authz model to handle groups and improve later…
bobeal Dec 9, 2021
9f55df6
feat(search): add support for attribute append and delete events from…
bobeal Dec 9, 2021
b893174
feat: add a WithKafkaContainer config / handle groups and stellio-adm…
bobeal Dec 22, 2021
c11a410
fix(search): add missing checks for clients
bobeal Dec 23, 2021
0d2abce
feat(search): add support for default role at subject creation time
bobeal Dec 23, 2021
92c62de
feat(search): implement entities filtering according to user's access…
bobeal Dec 27, 2021
c2090d3
feat: implement iam and rights synchronizer
bobeal Jan 2, 2022
7ca6485
feat(api-gw): add route for IAM sync endpoint
bobeal Jan 2, 2022
e311aff
fix(entity): reuse common constants in ApiBootstrapper
bobeal Jan 2, 2022
21b41ec
refactor(entity): cleanup and refactor IAMSynchronizer
bobeal Jan 2, 2022
bce65c5
refactor: move security context related variable in shared module
bobeal Jan 2, 2022
002c79e
refactor: move security specific constants into AuthUtils
bobeal Jan 3, 2022
6218bf2
qual(common): add some more tests
bobeal Jan 3, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,35 +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/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()
}
Expand Down
2 changes: 0 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down
5 changes: 2 additions & 3 deletions entity-service/config/detekt/baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<ID>LargeClass:EntityHandlerTests.kt$EntityHandlerTests</ID>
<ID>LargeClass:EntityOperationHandlerTests.kt$EntityOperationHandlerTests</ID>
<ID>LargeClass:EntityServiceTests.kt$EntityServiceTests</ID>
<ID>LargeClass:Neo4jRepositoryTests.kt$Neo4jRepositoryTests : WithNeo4jContainer</ID>
<ID>LargeClass:StandaloneNeo4jSearchRepositoryTests.kt$StandaloneNeo4jSearchRepositoryTests : WithNeo4jContainer</ID>
<ID>LargeClass:Neo4jRepositoryTests.kt$Neo4jRepositoryTests : WithNeo4jContainerWithKafkaContainer</ID>
<ID>LargeClass:StandaloneNeo4jSearchRepositoryTests.kt$StandaloneNeo4jSearchRepositoryTests : WithNeo4jContainerWithKafkaContainer</ID>
<ID>LongMethod:EntityEventServiceTests.kt$EntityEventServiceTests$@Test fun `it should publish ATTRIBUTE_APPEND and ATTRIBUTE_REPLACE events if attributes were appended and replaced`()</ID>
<ID>LongMethod:EntityHandler.kt$EntityHandler$ @GetMapping(produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun getEntities( @RequestHeader httpHeaders: HttpHeaders, @RequestParam params: MultiValueMap&lt;String, String&gt; ): ResponseEntity&lt;*&gt;</ID>
<ID>LongMethod:EntityServiceTests.kt$EntityServiceTests$@Test fun `it should create a new multi attribute property`()</ID>
Expand All @@ -16,7 +16,6 @@
<ID>LongParameterList:EntityEventService.kt$EntityEventService$( entityId: URI, entityType: String, attributeName: String, datasetId: URI? = null, overwrite: Boolean, operationPayload: String, updateOperationResult: UpdateOperationResult, contexts: List&lt;String&gt; )</ID>
<ID>LongParameterList:EntityService.kt$EntityService$( queryParams: QueryParams, userSub: String, offset: Int, limit: Int, contextLink: String, includeSysAttrs: Boolean )</ID>
<ID>LongParameterList:EntityService.kt$EntityService$( queryParams: QueryParams, userSub: String, offset: Int, limit: Int, contexts: List&lt;String&gt;, includeSysAttrs: Boolean )</ID>
<ID>MaxLineLength:EntityHandlerTests.kt$EntityHandlerTests$ </ID>
<ID>MaxLineLength:Neo4jRepository.kt$Neo4jRepository$ MATCH (a:</ID>
<ID>MaxLineLength:Neo4jRepository.kt$Neo4jRepository$ MATCH (entity:</ID>
<ID>ReturnCount:EntityHandler.kt$EntityHandler$ @GetMapping(produces = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun getEntities( @RequestHeader httpHeaders: HttpHeaders, @RequestParam params: MultiValueMap&lt;String, String&gt; ): ResponseEntity&lt;*&gt;</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +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 CLIENT_LABEL = AUTHORIZATION_ONTOLOGY + "Client"
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"
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<String> = setOf(ADMIN_ROLE_LABEL)
val CREATION_ROLES: Set<String> = setOf(CREATION_ROLE_LABEL).plus(ADMIN_ROLES)
val ADMIN_RIGHT: Set<String> = setOf(R_CAN_ADMIN)
val WRITE_RIGHT: Set<String> = setOf(R_CAN_WRITE).plus(ADMIN_RIGHT)
val READ_RIGHT: Set<String> = setOf(R_CAN_READ).plus(WRITE_RIGHT)
}

enum class SpecificAccessPolicy {
AUTH_READ,
AUTH_WRITE
}

fun userIsAdmin(userSub: String): Boolean
fun userCanCreateEntities(userSub: String): Boolean
fun filterEntitiesUserCanRead(entitiesId: List<URI>, userSub: String): List<URI>
Expand All @@ -40,4 +14,5 @@ interface AuthorizationService {
fun userIsAdminOfEntity(entityId: URI, userSub: String): Boolean
fun createAdminLink(entityId: URI, userSub: String)
fun createAdminLinks(entitiesId: List<URI>, userSub: String)
fun removeUserRightsOnEntity(entityId: URI, subjectId: URI): Int
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.egm.stellio.entity.authorization

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
import com.egm.stellio.entity.model.Relationship
import com.egm.stellio.shared.util.JsonLdUtils.EGM_SPECIFIC_ACCESS_POLICY
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.toListOfString
import com.egm.stellio.shared.util.toUri
import org.springframework.data.neo4j.core.Neo4jClient
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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:"$AUTH_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: "$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:"$EGM_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: "$EGM_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()

Expand Down Expand Up @@ -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
"""
Expand All @@ -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: ${'$'}subjectId })-[:HAS_OBJECT]-(relNode)
-[]->(target:Entity { id: ${'$'}targetId })
DETACH DELETE relNode
""".trimIndent()

val parameters = mapOf(
"subjectId" to subjectId.toString(),
"targetId" to targetId.toString()
)

return neo4jClient.query(matchQuery).bindAll(parameters).run().counters().nodesDeleted()
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
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 arrow.core.flattenOption
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
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.stereotype.Component
Expand All @@ -24,8 +26,10 @@ class Neo4jAuthorizationService(

override fun userCanCreateEntities(userSub: String): Boolean = userIsOneOfGivenRoles(CREATION_ROLES, userSub)

private fun userIsOneOfGivenRoles(roles: Set<String>, userSub: String): Boolean =
private fun userIsOneOfGivenRoles(roles: Set<GlobalRole>, userSub: String): Boolean =
neo4jAuthorizationRepository.getUserRoles((USER_PREFIX + userSub).toUri())
.map { GlobalRole.forKey(it) }
.flattenOption()
.intersect(roles)
.isNotEmpty()

Expand All @@ -35,10 +39,10 @@ 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),
READ_RIGHT,
entitiesId.minus(authorizedBySpecificPolicyEntities.toSet()),
READ_RIGHTS,
userSub
)
return authorizedBySpecificPolicyEntities.plus(grantedEntities)
Expand All @@ -47,17 +51,17 @@ class Neo4jAuthorizationService(
override fun filterEntitiesUserCanUpdate(entitiesId: List<URI>, userSub: String): List<URI> {
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),
WRITE_RIGHT,
entitiesId.minus(authorizedBySpecificPolicyEntities.toSet()),
WRITE_RIGHTS,
userSub
)
return authorizedBySpecificPolicyEntities.plus(grantedEntities)
}

override fun filterEntitiesUserCanAdmin(entitiesId: List<URI>, userSub: String): List<URI> =
filterEntitiesUserHaveOneOfGivenRights(entitiesId, ADMIN_RIGHT, userSub)
filterEntitiesUserHaveOneOfGivenRights(entitiesId, ADMIN_RIGHTS, userSub)

override fun splitEntitiesByUserCanAdmin(
entitiesId: List<URI>,
Expand Down Expand Up @@ -93,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,
Expand Down Expand Up @@ -136,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()
)
}
Expand All @@ -146,4 +150,7 @@ class Neo4jAuthorizationService(
entitiesId
)
}

override fun removeUserRightsOnEntity(entityId: URI, subjectId: URI) =
neo4jAuthorizationRepository.removeUserRightsOnEntity(subjectId, entityId)
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,6 @@ class StandaloneAuthorizationService : AuthorizationService {
override fun createAdminLink(entityId: URI, userSub: String) {}

override fun createAdminLinks(entitiesId: List<URI>, userSub: String) {}

override fun removeUserRightsOnEntity(entityId: URI, subjectId: URI): Int = 1
}
Loading