Skip to content

Commit

Permalink
refactor: entity and temporal handlers before adding support for dist…
Browse files Browse the repository at this point in the history
…Ops (#1219)

* refactor: introduce business oriented packaging (temporal/entities/discovery/common)
* refactor: rename EntityPayloadService -> EntityService
* refactor: rename EntityPayload to Entity and TemporalEntityAttribute to Attribute
* refactor: entity and temporal handlers
* fix(authz): delete user entities before deleting the user (if the user no longer exists, it fails because of access rights checks)
* refactor: remove service logic from EntityOperationHandler
(except upsert separation between create and update)
* refactor: upsert logic in service

---------

Co-authored-by: Thomas BOUSSELIN <[email protected]>
  • Loading branch information
bobeal and thomasBousselin authored Aug 30, 2024
1 parent a740e14 commit 7d9c62f
Show file tree
Hide file tree
Showing 117 changed files with 4,648 additions and 5,447 deletions.
23 changes: 11 additions & 12 deletions search-service/config/detekt/baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,27 @@
<ID>ClassNaming:V0_29_JsonLd_migrationTests.kt$V0_29_JsonLd_migrationTests</ID>
<ID>ClassNaming:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration : BaseJavaMigration</ID>
<ID>ComplexCondition:EntitiesQueryUtils.kt$geoQuery == null &amp;&amp; q.isNullOrEmpty() &amp;&amp; typeSelection.isNullOrEmpty() &amp;&amp; attrs.isEmpty()</ID>
<ID>ComplexCondition:EntityPayloadService.kt$EntityPayloadService$it &amp;&amp; !inverse || !it &amp;&amp; inverse</ID>
<ID>ComplexCondition:EntityQueryService.kt$EntityQueryService$it &amp;&amp; !inverse || !it &amp;&amp; inverse</ID>
<ID>Filename:V0_29__JsonLd_migration.kt$db.migration.V0_29__JsonLd_migration.kt</ID>
<ID>LongMethod:AttributeInstanceService.kt$AttributeInstanceService$@Transactional suspend fun create(attributeInstance: AttributeInstance): Either&lt;APIException, Unit&gt;</ID>
<ID>LongMethod:EnabledAuthorizationServiceTests.kt$EnabledAuthorizationServiceTests$@Test fun `it should return serialized access control entities with other rigths if user is owner`()</ID>
<ID>LongMethod:EntityAccessControlHandler.kt$EntityAccessControlHandler$@PostMapping("/{subjectId}/attrs", consumes = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun addRightsOnEntities( @RequestHeader httpHeaders: HttpHeaders, @PathVariable subjectId: String, @RequestBody requestBody: Mono&lt;String&gt; ): ResponseEntity&lt;*&gt;</ID>
<ID>LongMethod:PatchAttributeTests.kt$PatchAttributeTests.Companion$@JvmStatic fun mergePatchProvider(): Stream&lt;Arguments&gt;</ID>
<ID>LongMethod:PatchAttributeTests.kt$PatchAttributeTests.Companion$@JvmStatic fun partialUpdatePatchProvider(): Stream&lt;Arguments&gt;</ID>
<ID>LongMethod:QueryServiceTests.kt$QueryServiceTests$@Test fun `it should query temporal entities as requested by query params`()</ID>
<ID>LongMethod:QueryServiceTests.kt$QueryServiceTests$@Test fun `it should return an empty list for an attribute if it has no temporal values`()</ID>
<ID>LongMethod:TemporalQueryServiceTests.kt$TemporalQueryServiceTests$@Test fun `it should query temporal entities as requested by query params`()</ID>
<ID>LongMethod:TemporalQueryServiceTests.kt$TemporalQueryServiceTests$@Test fun `it should return an empty list for an attribute if it has no temporal values`()</ID>
<ID>LongMethod:TemporalScopeBuilderTests.kt$TemporalScopeBuilderTests$@Test fun `it should build an aggregated temporal representation of scopes`()</ID>
<ID>LongMethod:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration$override fun migrate(context: Context)</ID>
<ID>LongParameterList:AttributeInstance.kt$AttributeInstance.Companion$( temporalEntityAttribute: UUID, instanceId: URI = generateRandomInstanceId(), timeAndProperty: Pair&lt;ZonedDateTime, TemporalProperty&gt;, value: Triple&lt;String?, Double?, WKTCoordinates?&gt;, payload: ExpandedAttributeInstance, sub: String? )</ID>
<ID>LongParameterList:AttributeInstance.kt$AttributeInstance.Companion$( temporalEntityAttribute: UUID, instanceId: URI = generateRandomInstanceId(), timeProperty: TemporalProperty? = TemporalProperty.OBSERVED_AT, modifiedAt: ZonedDateTime? = null, attributeMetadata: AttributeMetadata, payload: ExpandedAttributeInstance, time: ZonedDateTime, sub: String? = null )</ID>
<ID>LongParameterList:AttributeInstance.kt$AttributeInstance.Companion$( attributeUuid: UUID, instanceId: URI = generateRandomInstanceId(), timeAndProperty: Pair&lt;ZonedDateTime, TemporalProperty&gt;, value: Triple&lt;String?, Double?, WKTCoordinates?&gt;, payload: ExpandedAttributeInstance, sub: String? )</ID>
<ID>LongParameterList:AttributeInstance.kt$AttributeInstance.Companion$( attributeUuid: UUID, instanceId: URI = generateRandomInstanceId(), timeProperty: TemporalProperty? = TemporalProperty.OBSERVED_AT, modifiedAt: ZonedDateTime? = null, attributeMetadata: AttributeMetadata, payload: ExpandedAttributeInstance, time: ZonedDateTime, sub: String? = null )</ID>
<ID>LongParameterList:EntityAttributeService.kt$EntityAttributeService$( attribute: Attribute, attributeName: ExpandedTerm, attributeMetadata: AttributeMetadata, mergedAt: ZonedDateTime, observedAt: ZonedDateTime?, attributePayload: ExpandedAttributeInstance, sub: Sub? )</ID>
<ID>LongParameterList:EntityAttributeService.kt$EntityAttributeService$( attribute: Attribute, ngsiLdAttribute: NgsiLdAttribute, attributeMetadata: AttributeMetadata, createdAt: ZonedDateTime, attributePayload: ExpandedAttributeInstance, sub: Sub? )</ID>
<ID>LongParameterList:EntityAttributeService.kt$EntityAttributeService$( entityId: URI, attributeName: ExpandedTerm, attributeMetadata: AttributeMetadata, createdAt: ZonedDateTime, attributePayload: ExpandedAttributeInstance, sub: Sub? )</ID>
<ID>LongParameterList:EntityAttributeService.kt$EntityAttributeService$( entityUri: URI, ngsiLdAttributes: List&lt;NgsiLdAttribute&gt;, expandedAttributes: ExpandedAttributes, createdAt: ZonedDateTime, observedAt: ZonedDateTime?, sub: Sub? )</ID>
<ID>LongParameterList:EntityAttributeService.kt$EntityAttributeService$( entityUri: URI, ngsiLdAttributes: List&lt;NgsiLdAttribute&gt;, expandedAttributes: ExpandedAttributes, disallowOverwrite: Boolean, createdAt: ZonedDateTime, sub: Sub? )</ID>
<ID>LongParameterList:EntityEventService.kt$EntityEventService$( updatedDetails: UpdatedDetails, sub: String?, tenantName: String, entityId: URI, entityTypesAndPayload: Pair&lt;List&lt;ExpandedTerm&gt;, String&gt;, serializedAttribute: Pair&lt;ExpandedTerm, String&gt;, overwrite: Boolean )</ID>
<ID>LongParameterList:TemporalEntityAttributeService.kt$TemporalEntityAttributeService$( entityId: URI, attributeName: ExpandedTerm, attributeMetadata: AttributeMetadata, createdAt: ZonedDateTime, attributePayload: ExpandedAttributeInstance, sub: Sub? )</ID>
<ID>LongParameterList:TemporalEntityAttributeService.kt$TemporalEntityAttributeService$( entityUri: URI, ngsiLdAttributes: List&lt;NgsiLdAttribute&gt;, expandedAttributes: ExpandedAttributes, createdAt: ZonedDateTime, observedAt: ZonedDateTime?, sub: Sub? )</ID>
<ID>LongParameterList:TemporalEntityAttributeService.kt$TemporalEntityAttributeService$( entityUri: URI, ngsiLdAttributes: List&lt;NgsiLdAttribute&gt;, expandedAttributes: ExpandedAttributes, disallowOverwrite: Boolean, createdAt: ZonedDateTime, sub: Sub? )</ID>
<ID>LongParameterList:TemporalEntityAttributeService.kt$TemporalEntityAttributeService$( tea: TemporalEntityAttribute, attributeName: ExpandedTerm, attributeMetadata: AttributeMetadata, mergedAt: ZonedDateTime, observedAt: ZonedDateTime?, attributePayload: ExpandedAttributeInstance, sub: Sub? )</ID>
<ID>LongParameterList:TemporalEntityAttributeService.kt$TemporalEntityAttributeService$( temporalEntityAttribute: TemporalEntityAttribute, ngsiLdAttribute: NgsiLdAttribute, attributeMetadata: AttributeMetadata, createdAt: ZonedDateTime, attributePayload: ExpandedAttributeInstance, sub: Sub? )</ID>
<ID>LongParameterList:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration$( entityId: URI, attributeName: ExpandedTerm, datasetId: URI?, attributePayload: ExpandedAttributeInstance, ngsiLdAttributeInstance: NgsiLdAttributeInstance, defaultCreatedAt: ZonedDateTime )</ID>
<ID>NestedBlockDepth:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration$override fun migrate(context: Context)</ID>
<ID>SwallowedException:EntitiesQueryUtils.kt$e: IllegalArgumentException</ID>
<ID>TooManyFunctions:EntityPayloadService.kt$EntityPayloadService</ID>
<ID>SwallowedException:TemporalQueryUtils.kt$e: IllegalArgumentException</ID>
</CurrentIssues>
</SmellBaseline>
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication

@SpringBootApplication(scanBasePackages = ["com.egm.stellio.search", "com.egm.stellio.shared"])
@ConfigurationPropertiesScan("com.egm.stellio.search.config", "com.egm.stellio.shared.config")
@ConfigurationPropertiesScan("com.egm.stellio.search.common.config", "com.egm.stellio.shared.config")
class SearchServiceApplication

@Suppress("SpreadOperator")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
package com.egm.stellio.search.listener
package com.egm.stellio.search.authorization.listener

import arrow.core.Either
import arrow.core.flattenOption
import arrow.core.left
import arrow.core.raise.either
import arrow.core.right
import com.egm.stellio.search.authorization.EntityAccessRightsService
import com.egm.stellio.search.authorization.SubjectReferential
import com.egm.stellio.search.authorization.SubjectReferentialService
import com.egm.stellio.search.authorization.toSubjectInfo
import com.egm.stellio.search.config.SearchProperties
import com.egm.stellio.search.service.EntityEventService
import com.egm.stellio.search.service.EntityPayloadService
import com.egm.stellio.search.authorization.model.SubjectReferential
import com.egm.stellio.search.authorization.model.toSubjectInfo
import com.egm.stellio.search.authorization.service.EntityAccessRightsService
import com.egm.stellio.search.authorization.service.SubjectReferentialService
import com.egm.stellio.search.common.config.SearchProperties
import com.egm.stellio.search.entity.service.EntityService
import com.egm.stellio.shared.model.*
import com.egm.stellio.shared.util.AuthContextModel.AUTH_TERM_IS_MEMBER_OF
import com.egm.stellio.shared.util.AuthContextModel.AUTH_TERM_ROLES
Expand Down Expand Up @@ -39,8 +38,7 @@ class IAMListener(
private val subjectReferentialService: SubjectReferentialService,
private val searchProperties: SearchProperties,
private val entityAccessRightsService: EntityAccessRightsService,
private val entityPayloadService: EntityPayloadService,
private val entityEventService: EntityEventService
private val entityService: EntityService
) {

private val logger = LoggerFactory.getLogger(javaClass)
Expand Down Expand Up @@ -114,19 +112,15 @@ class IAMListener(
val subjectType = SubjectType.valueOf(entityDeleteEvent.entityTypes.first().uppercase())
val sub = entityDeleteEvent.entityId.extractSub()
mono {
// delete the entities owned by the user while the user still exists
// (if it no longer exists, it fails because of access rights checks)
if (searchProperties.onOwnerDeleteCascadeEntities && subjectType == SubjectType.USER) {
entityAccessRightsService.getEntitiesIdsOwnedBySubject(sub).getOrNull()?.forEach { entityId ->
entityService.deleteEntity(entityId, sub)
}
Unit.right()
} else Unit.right()
subjectReferentialService.delete(entityDeleteEvent.entityId.extractSub())
val result: Either<APIException, Unit> =
if (searchProperties.onOwnerDeleteCascadeEntities && subjectType == SubjectType.USER) {
val entitiesIds = entityAccessRightsService.getEntitiesIdsOwnedBySubject(sub).getOrNull()
entitiesIds?.let { entityAccessRightsService.deleteAllAccessRightsOnEntities(it) }
entitiesIds?.forEach { entityId ->
entityPayloadService.deleteEntity(entityId).getOrNull()?.also {
entityEventService.publishEntityDeleteEvent(null, it)
}
}
Unit.right()
} else Unit.right()
result
}.writeContextAndSubscribe(tenantName, entityDeleteEvent)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.egm.stellio.search.authorization
package com.egm.stellio.search.authorization.model

import com.egm.stellio.shared.model.ExpandedAttributeInstances
import com.egm.stellio.shared.model.ExpandedTerm
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.egm.stellio.search.authorization
package com.egm.stellio.search.authorization.model

import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_NAME
import com.egm.stellio.shared.util.AuthContextModel.AUTH_REL_IS_MEMBER_OF
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.egm.stellio.search.authorization
package com.egm.stellio.search.authorization.model

import com.egm.stellio.shared.util.AccessRight
import java.net.URI
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.egm.stellio.search.authorization
package com.egm.stellio.search.authorization.model

import com.egm.stellio.search.util.deserializeAsMap
import com.egm.stellio.search.common.util.deserializeAsMap
import com.egm.stellio.shared.util.GlobalRole
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_VALUE_TERM
import com.egm.stellio.shared.util.JsonUtils
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.egm.stellio.search.authorization
package com.egm.stellio.search.authorization.model

import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_FAMILY_NAME
import com.egm.stellio.shared.util.AuthContextModel.AUTH_PROP_GIVEN_NAME
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.egm.stellio.search.authorization
package com.egm.stellio.search.authorization.service

import arrow.core.Either
import arrow.core.Option
import com.egm.stellio.search.model.EntitiesQuery
import com.egm.stellio.search.entity.model.EntitiesQuery
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.ExpandedEntity
import com.egm.stellio.shared.util.Sub
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.egm.stellio.search.authorization
package com.egm.stellio.search.authorization.service

import arrow.core.Either
import arrow.core.Option
import arrow.core.right
import com.egm.stellio.search.model.EntitiesQuery
import com.egm.stellio.search.entity.model.EntitiesQuery
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.ExpandedEntity
import com.egm.stellio.shared.util.Sub
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.egm.stellio.search.authorization
package com.egm.stellio.search.authorization.service

import arrow.core.*
import arrow.core.raise.either
import arrow.fx.coroutines.parMap
import com.egm.stellio.search.model.EntitiesQuery
import com.egm.stellio.search.entity.model.EntitiesQuery
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.AccessDeniedException
import com.egm.stellio.shared.model.ExpandedEntity
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.egm.stellio.search.authorization
package com.egm.stellio.search.authorization.service

import arrow.core.*
import arrow.core.raise.either
import com.egm.stellio.search.authorization.EntityAccessRights.SubjectRightInfo
import com.egm.stellio.search.service.EntityPayloadService
import com.egm.stellio.search.util.*
import com.egm.stellio.search.authorization.model.EntityAccessRights
import com.egm.stellio.search.authorization.model.EntityAccessRights.SubjectRightInfo
import com.egm.stellio.search.common.util.*
import com.egm.stellio.shared.config.ApplicationProperties
import com.egm.stellio.shared.model.*
import com.egm.stellio.shared.util.*
Expand All @@ -30,8 +30,7 @@ import java.net.URI
class EntityAccessRightsService(
private val applicationProperties: ApplicationProperties,
private val databaseClient: DatabaseClient,
private val subjectReferentialService: SubjectReferentialService,
private val entityPayloadService: EntityPayloadService
private val subjectReferentialService: SubjectReferentialService
) {
@Transactional
suspend fun setReadRoleOnEntity(sub: Sub, entityId: URI): Either<APIException, Unit> =
Expand Down Expand Up @@ -143,7 +142,7 @@ class EntityAccessRightsService(
subjectReferentialService.hasStellioAdminRole(subjectUuids)
.flatMap {
if (!it)
entityPayloadService.hasSpecificAccessPolicies(entityId, specificAccessPolicies)
hasSpecificAccessPolicies(entityId, specificAccessPolicies)
else true.right()
}.flatMap {
if (!it)
Expand All @@ -152,6 +151,26 @@ class EntityAccessRightsService(
}.bind()
}

suspend fun hasSpecificAccessPolicies(
entityId: URI,
specificAccessPolicies: List<SpecificAccessPolicy>
): Either<APIException, Boolean> {
if (specificAccessPolicies.isEmpty())
return either { false }

return databaseClient.sql(
"""
SELECT count(entity_id) as count
FROM entity_payload
WHERE entity_id = :entity_id
AND specific_access_policy IN (:specific_access_policies)
""".trimIndent()
)
.bind("entity_id", entityId)
.bind("specific_access_policies", specificAccessPolicies.map { it.toString() })
.oneToResult { it["count"] as Long > 0 }
}

private suspend fun hasDirectAccessRightOnEntity(
uuids: List<Sub>,
entityId: URI,
Expand Down Expand Up @@ -337,6 +356,35 @@ class EntityAccessRightsService(
.allToMappedList { toUri(it["entity_id"]) }
}

suspend fun updateSpecificAccessPolicy(
entityId: URI,
ngsiLdAttribute: NgsiLdAttribute
): Either<APIException, Unit> = either {
val specificAccessPolicy = ngsiLdAttribute.getSpecificAccessPolicy().bind()
databaseClient.sql(
"""
UPDATE entity_payload
SET specific_access_policy = :specific_access_policy
WHERE entity_id = :entity_id
""".trimIndent()
)
.bind("entity_id", entityId)
.bind("specific_access_policy", specificAccessPolicy.toString())
.execute()
.bind()
}

suspend fun removeSpecificAccessPolicy(entityId: URI): Either<APIException, Unit> =
databaseClient.sql(
"""
UPDATE entity_payload
SET specific_access_policy = null
WHERE entity_id = :entity_id
""".trimIndent()
)
.bind("entity_id", entityId)
.execute()

@Transactional
suspend fun delete(sub: Sub): Either<APIException, Unit> =
databaseClient
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.egm.stellio.search.authorization
package com.egm.stellio.search.authorization.service

import arrow.core.Either
import arrow.core.Option
import arrow.core.Some
import arrow.core.getOrElse
import com.egm.stellio.search.util.*
import com.egm.stellio.search.authorization.model.Group
import com.egm.stellio.search.authorization.model.SubjectReferential
import com.egm.stellio.search.authorization.model.User
import com.egm.stellio.search.common.util.*
import com.egm.stellio.shared.model.APIException
import com.egm.stellio.shared.model.AccessDeniedException
import com.egm.stellio.shared.util.*
Expand Down
Loading

0 comments on commit 7d9c62f

Please sign in to comment.