Skip to content

Commit

Permalink
feat: add a WithKafkaContainer config / handle groups and stellio-adm…
Browse files Browse the repository at this point in the history
…in access checks
  • Loading branch information
bobeal committed Dec 22, 2021
1 parent df12022 commit 3a54fb9
Show file tree
Hide file tree
Showing 29 changed files with 302 additions and 75 deletions.
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.1")
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 @@ -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
Expand All @@ -29,7 +30,7 @@ import java.net.URI

@SpringBootTest
@ActiveProfiles("test")
class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer {
class Neo4jAuthorizationRepositoryTest : WithNeo4jContainer, WithKafkaContainer {

@Autowired
private lateinit var neo4jAuthorizationRepository: Neo4jAuthorizationRepository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -13,7 +14,7 @@ import java.net.URI

@SpringBootTest
@ActiveProfiles("test")
class EntityRepositoryTests : WithNeo4jContainer {
class EntityRepositoryTests : WithNeo4jContainer, WithKafkaContainer {

@Autowired
private lateinit var entityRepository: EntityRepository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,7 +29,7 @@ import java.net.URI

@SpringBootTest
@ActiveProfiles("test")
class Neo4jRepositoryTests : WithNeo4jContainer {
class Neo4jRepositoryTests : WithNeo4jContainer, WithKafkaContainer {

@Autowired
private lateinit var neo4jRepository: Neo4jRepository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)
Expand All @@ -70,30 +71,51 @@ class EntityAccessRightsService(
.thenReturn(1)
.onErrorReturn(-1)

fun hasReadRoleOnEntity(subjectId: UUID, entityId: URI): Mono<Boolean> =
hasRoleOnEntity(subjectId, entityId, listOf(R_CAN_READ, R_CAN_WRITE, R_CAN_ADMIN))
fun canReadEntity(subjectId: UUID, entityId: URI): Mono<Boolean> =
checkHasAccessRight(subjectId, entityId, listOf(R_CAN_READ, R_CAN_WRITE, R_CAN_ADMIN))

fun hasWriteRoleOnEntity(subjectId: UUID, entityId: URI): Mono<Boolean> =
hasRoleOnEntity(subjectId, entityId, listOf(R_CAN_WRITE, R_CAN_ADMIN))
fun canWriteEntity(subjectId: UUID, entityId: URI): Mono<Boolean> =
checkHasAccessRight(subjectId, entityId, listOf(R_CAN_WRITE, R_CAN_ADMIN))

fun hasRoleOnEntity(subjectId: UUID, entityId: URI, accessRights: List<AccessRight>): Mono<Boolean> =
private fun checkHasAccessRight(subjectId: UUID, entityId: URI, accessRights: List<AccessRight>): Mono<Boolean> =
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<UUID>, entityId: URI, accessRights: List<AccessRight>): Mono<Boolean> =
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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ class IAMListener(
subjectUuid,
groupId.extractSubjectUuid()
)
} else {
logger.info("Received unknown attribute name: ${attributeAppendEvent.attributeName}")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -57,7 +57,29 @@ class SubjectReferentialService(
.bind("subject_id", subjectId)
.fetch()
.one()
.map { rowToUserAccessRights(it) }
.map { rowToSubjectReferential(it) }

fun hasStellioAdminRole(subjectId: UUID): Mono<Boolean> =
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<GlobalRole>): Mono<Int> =
Expand All @@ -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)
Expand Down Expand Up @@ -161,12 +183,12 @@ class SubjectReferentialService(
.matching(Query.query(Criteria.where("subject_id").`is`(subjectId)))
.all()

private fun rowToUserAccessRights(row: Map<String, Any>) =
private fun rowToSubjectReferential(row: Map<String, Any>) =
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<String>?)?.map { GlobalRole.valueOf(it) },
globalRoles = (row["global_roles"] as Array<String>?)?.map { GlobalRole.forKey(it) },
groupsMemberships = (row["groups_memberships"] as Array<String>?)?.map { it.toUUID() }
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -37,7 +38,7 @@ import kotlin.random.Random

@SpringBootTest
@ActiveProfiles("test")
class AttributeInstanceServiceTests : WithTimescaleContainer {
class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer {

@Autowired
private lateinit var attributeInstanceService: AttributeInstanceService
Expand Down
Loading

0 comments on commit 3a54fb9

Please sign in to comment.