From 416863e747e7c1e64c3f7374a71c826eeb737f4f Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 6 Aug 2024 14:29:56 +0200 Subject: [PATCH] Allow to add exceptions to schema enforcement (#5075) Co-authored-by: Simon Dumas --- delta/app/src/main/resources/app.conf | 5 + .../nexus/delta/wiring/ResourcesModule.scala | 6 +- .../resources/errors/invalid-resource.json | 32 ----- .../schemas/errors/invalid-schema-2.json | 13 -- .../delta/routes/ResourcesRoutesSpec.scala | 73 +++------- .../delta/sdk/resources/ResourcesConfig.scala | 25 +++- .../delta/sdk/resources/SchemaClaim.scala | 37 +---- .../sdk/resources/SchemaClaimResolver.scala | 74 ++++++++++ .../sdk/resources/ValidateResource.scala | 28 +--- .../sdk/resources/ResourcesImplSpec.scala | 122 ++++++---------- .../resources/SchemaClaimResolverSuite.scala | 131 ++++++++++++++++++ .../sdk/resources/SchemaClaimSuite.scala | 70 ---------- .../resources/ValidateResourceFixture.scala | 32 ++++- .../sdk/resources/ValidateResourceSuite.scala | 26 +--- ship/src/main/resources/ship-default.conf | 3 - .../epfl/bluebrain/nexus/ship/RunShip.scala | 4 +- .../nexus/ship/config/InputConfig.scala | 1 - .../nexus/ship/resources/ResourceWiring.scala | 19 +-- .../ship/config/ShipConfigFixtures.scala | 1 - tests/docker/config/delta-postgres.conf | 7 + .../tests/kg/SearchConfigIndexingSpec.scala | 9 +- .../tests/resources/EnforcedSchemaSpec.scala | 42 +++++- 22 files changed, 403 insertions(+), 357 deletions(-) delete mode 100644 delta/app/src/test/resources/resources/errors/invalid-resource.json create mode 100644 delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/SchemaClaimResolver.scala create mode 100644 delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/SchemaClaimResolverSuite.scala delete mode 100644 delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/SchemaClaimSuite.scala diff --git a/delta/app/src/main/resources/app.conf b/delta/app/src/main/resources/app.conf index c76d59b3d7..4bcea20f0d 100644 --- a/delta/app/src/main/resources/app.conf +++ b/delta/app/src/main/resources/app.conf @@ -269,6 +269,11 @@ app { event-log = ${app.defaults.event-log} # Reject payloads which contain nexus metadata fields (any field beginning with _) decoding-option = "strict" + # Defines exceptions for schema enforcement + schema-enforcement { + type-whitelist = [] + allow-no-types = false + } # Do not create a new revision of a resource when the update does not introduce a change skip-update-no-change = true } diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ResourcesModule.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ResourcesModule.scala index 521a352d71..6465132ced 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ResourcesModule.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ResourcesModule.scala @@ -37,8 +37,10 @@ object ResourcesModule extends ModuleDef { ResourceResolution.schemaResource(aclCheck, resolvers, fetchSchema, excludeDeprecated = false) } - make[ValidateResource].from { (resourceResolution: ResourceResolution[Schema], validateShacl: ValidateShacl) => - ValidateResource(resourceResolution, validateShacl) + make[ValidateResource].from { + (resourceResolution: ResourceResolution[Schema], validateShacl: ValidateShacl, config: ResourcesConfig) => + val schemaClaimResolver = SchemaClaimResolver(resourceResolution, config.schemaEnforcement) + ValidateResource(schemaClaimResolver, validateShacl) } make[ResourcesConfig].from { (config: AppConfig) => config.resources } diff --git a/delta/app/src/test/resources/resources/errors/invalid-resource.json b/delta/app/src/test/resources/resources/errors/invalid-resource.json deleted file mode 100644 index 216e1aec37..0000000000 --- a/delta/app/src/test/resources/resources/errors/invalid-resource.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "@context": [ - "https://bluebrain.github.io/nexus/contexts/shacl-20170720.json", - "https://bluebrain.github.io/nexus/contexts/error.json" - ], - "@type": "InvalidResource", - "reason": "Resource 'https://bluebrain.github.io/nexus/vocabulary/wrong' failed to validate against the constraints defined in schema 'https://bluebrain.github.io/nexus/vocabulary/myschema?rev=1'", - "details": { - "@type": "sh:ValidationReport", - "conforms": false, - "result": { - "@type": "sh:ValidationResult", - "focusNode": "nxv:wrong", - "result:Path": { - "@id": "nxv:number" - }, - "resultMessage": "Value must be a valid literal of type integer", - "resultSeverity": "sh:Violation", - "sh:value": "wrong", - "sourceConstraintComponent": "sh:DatatypeConstraintComponent", - "sourceShape": "nxv:NumberProperty" - }, - "targetedNodes": 10 - }, - "expanded": { - "@id": "nxv:wrong", - "@type": "schema:Custom", - "bool": false, - "number": "wrong", - "nxv:name": "Alex" - } -} \ No newline at end of file diff --git a/delta/app/src/test/resources/schemas/errors/invalid-schema-2.json b/delta/app/src/test/resources/schemas/errors/invalid-schema-2.json index 741ecddd9a..4c9cb0beca 100644 --- a/delta/app/src/test/resources/schemas/errors/invalid-schema-2.json +++ b/delta/app/src/test/resources/schemas/errors/invalid-schema-2.json @@ -7,19 +7,6 @@ "reason": "Schema 'https://bluebrain.github.io/nexus/vocabulary/pretendschema' could not be resolved in 'myorg/myproject'", "report": { "history": [ - { - "rejections": [ - { - "cause": { - "@type": "ResourceNotFound", - "reason": "The resource was not found in project 'myorg/myproject'." - }, - "project": "myorg/myproject" - } - ], - "resolverId": "https://bluebrain.github.io/nexus/vocabulary/in-project", - "success": false - } ] } } \ No newline at end of file diff --git a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala index 0ec6e2b269..3b6bdf80c5 100644 --- a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala +++ b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ResourcesRoutesSpec.scala @@ -4,21 +4,18 @@ import akka.http.scaladsl.model.MediaTypes.`text/html` import akka.http.scaladsl.model.headers.{Accept, Location, OAuth2BearerToken, RawHeader} import akka.http.scaladsl.model.{RequestEntity, StatusCodes, Uri} import akka.http.scaladsl.server.Route -import cats.effect.IO import cats.implicits._ import ch.epfl.bluebrain.nexus.delta.kernel.utils.{UUIDF, UrlUtils} import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.{contexts, nxv, schema => schemaOrg, schemas} import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.JsonLdContext.keywords -import ch.epfl.bluebrain.nexus.delta.rdf.shacl.ValidateShacl import ch.epfl.bluebrain.nexus.delta.sdk.IndexingAction import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclSimpleCheck import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.AclAddress -import ch.epfl.bluebrain.nexus.delta.sdk.generators.{ProjectGen, ResourceResolutionGen, SchemaGen} +import ch.epfl.bluebrain.nexus.delta.sdk.generators.ProjectGen import ch.epfl.bluebrain.nexus.delta.sdk.identities.IdentitiesDummy import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.implicits._ -import ch.epfl.bluebrain.nexus.delta.sdk.model.Fetch.FetchF import ch.epfl.bluebrain.nexus.delta.sdk.model.{IdSegmentRef, ResourceUris} import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.resources import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContextDummy @@ -26,19 +23,18 @@ import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ApiMappings import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution import ch.epfl.bluebrain.nexus.delta.sdk.resources.NexusSource.DecodingOption import ch.epfl.bluebrain.nexus.delta.sdk.resources._ -import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema import ch.epfl.bluebrain.nexus.delta.sdk.utils.BaseRouteSpec import ch.epfl.bluebrain.nexus.delta.sourcing.ScopedEventLog import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group, Subject, User} +import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag -import ch.epfl.bluebrain.nexus.delta.sourcing.model.{ProjectRef, ResourceRef} import ch.epfl.bluebrain.nexus.testkit.scalatest.ce.CatsIOValues import io.circe.{Json, Printer} import org.scalatest.Assertion import java.util.UUID -class ResourcesRoutesSpec extends BaseRouteSpec with CatsIOValues { +class ResourcesRoutesSpec extends BaseRouteSpec with ValidateResourceFixture with CatsIOValues { private val uuid = UUID.randomUUID() implicit private val uuidF: UUIDF = UUIDF.fixed(uuid) @@ -54,9 +50,9 @@ class ResourcesRoutesSpec extends BaseRouteSpec with CatsIOValues { private val asReader = addCredentials(OAuth2BearerToken("reader")) private val asWriter = addCredentials(OAuth2BearerToken("writer")) - private val am = ApiMappings("nxv" -> nxv.base, "Person" -> schemaOrg.Person) - private val projBase = nxv.base - private val project = ProjectGen.resourceFor( + private val am = ApiMappings("nxv" -> nxv.base, "Person" -> schemaOrg.Person) + private val projBase = nxv.base + private val project = ProjectGen.resourceFor( ProjectGen.project( "myorg", "myproject", @@ -66,12 +62,10 @@ class ResourcesRoutesSpec extends BaseRouteSpec with CatsIOValues { mappings = am + Resources.mappings ) ) - private val projectRef = project.value.ref - private val schemaSource = jsonContentOf("resources/schema.json").addContext(contexts.shacl, contexts.schemasMetadata) - private val schema1 = SchemaGen.schema(nxv + "myschema", project.value.ref, schemaSource.removeKeys(keywords.id)) - private val schema2 = SchemaGen.schema(schemaOrg.Person, project.value.ref, schemaSource.removeKeys(keywords.id)) - private val schema3 = SchemaGen.schema(nxv + "otherSchema", project.value.ref, schemaSource.removeKeys(keywords.id)) - private val tag = UserTag.unsafe("mytag") + private val projectRef = project.value.ref + private val schema1 = nxv + "myschema" + private val schema2 = nxv + "otherSchema" + private val tag = UserTag.unsafe("mytag") private val myId = nxv + "myid" // Resource created against no schema with id present on the payload private def encodeWithBase(id: String) = UrlUtils.encode((nxv + id).toString) @@ -89,27 +83,13 @@ class ResourcesRoutesSpec extends BaseRouteSpec with CatsIOValues { private val aclCheck = AclSimpleCheck().accepted - private val fetchSchema: (ResourceRef, ProjectRef) => FetchF[Schema] = { - case (ref, _) if ref.iri == schema2.id => IO.pure(Some(SchemaGen.resourceFor(schema2, deprecated = true))) - case (ref, _) if ref.iri == schema1.id => IO.pure(Some(SchemaGen.resourceFor(schema1))) - case (ref, _) if ref.iri == schema3.id => IO.pure(Some(SchemaGen.resourceFor(schema3))) - case _ => IO.none - } - - private val validator: ValidateResource = ValidateResource( - ResourceResolutionGen.singleInProject(projectRef, fetchSchema), - ValidateShacl(rcr).accepted - ) + private val validateResource = validateFor(Set((projectRef, schema1), (projectRef, schema2))) private val fetchContext = FetchContextDummy(List(project.value)) private val resolverContextResolution: ResolverContextResolution = ResolverContextResolution(rcr) private def routesWithDecodingOption(implicit decodingOption: DecodingOption): (Route, Resources) = { - val resourceDef = Resources.definition(validator, DetectChange(enabled = true), clock) - val scopedLog = ScopedEventLog( - resourceDef, - ResourcesConfig(eventLogConfig, decodingOption, skipUpdateNoChange = true).eventLog, - xas - ) + val resourceDef = Resources.definition(validateResource, DetectChange(enabled = true), clock) + val scopedLog = ScopedEventLog(resourceDef, eventLogConfig, xas) val resources = ResourcesImpl( scopedLog, @@ -158,7 +138,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with CatsIOValues { "create a resource" in { val endpoints = List( ("/v1/resources/myorg/myproject", schemas.resources), - ("/v1/resources/myorg/myproject/myschema", schema1.id) + ("/v1/resources/myorg/myproject/myschema", schema1) ) forAll(endpoints) { case (endpoint, schema) => val id = genString() @@ -172,7 +152,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with CatsIOValues { "create a tagged resource" in { val endpoints = List( ("/v1/resources/myorg/myproject?tag=mytag", schemas.resources), - ("/v1/resources/myorg/myproject/myschema?tag=mytag", schema1.id) + ("/v1/resources/myorg/myproject/myschema?tag=mytag", schema1) ) val (routes, resources) = routesWithDecodingOption(DecodingOption.Strict) forAll(endpoints) { case (endpoint, schema) => @@ -188,7 +168,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with CatsIOValues { "create a resource with an authenticated user and provided id" in { val endpoints = List( ((id: String) => s"/v1/resources/myorg/myproject/_/$id", schemas.resources), - ((id: String) => s"/v1/resources/myorg/myproject/myschema/$id", schema1.id) + ((id: String) => s"/v1/resources/myorg/myproject/myschema/$id", schema1) ) forAll(endpoints) { case (endpoint, schema) => val id = genString() @@ -202,7 +182,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with CatsIOValues { "create a tagged resource with an authenticated user and provided id" in { val endpoints = List( ((id: String) => s"/v1/resources/myorg/myproject/_/$id?tag=mytag", schemas.resources), - ((id: String) => s"/v1/resources/myorg/myproject/myschema/$id?tag=mytag", schema1.id) + ((id: String) => s"/v1/resources/myorg/myproject/myschema/$id?tag=mytag", schema1) ) val (routes, resources) = routesWithDecodingOption(DecodingOption.Strict) forAll(endpoints) { case (endpoint, schema) => @@ -228,17 +208,6 @@ class ResourcesRoutesSpec extends BaseRouteSpec with CatsIOValues { } } - "fail to create a resource that does not validate against a schema" in { - val payloadFailingSchemaConstraints = payloadWithoutId.replaceKeyWithValue("number", "wrong") - Put( - "/v1/resources/myorg/myproject/nxv:myschema/wrong", - payloadFailingSchemaConstraints.toEntity - ) ~> asWriter ~> routes ~> check { - response.status shouldEqual StatusCodes.BadRequest - response.asJson shouldEqual jsonContentOf("resources/errors/invalid-resource.json") - } - } - "fail to create a resource against a schema that does not exist" in { Put( "/v1/resources/myorg/myproject/pretendschema/someid", @@ -296,7 +265,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with CatsIOValues { forAll(endpoints) { case (endpoint, rev) => Put(s"$endpoint?rev=$rev", payloadUpdated(id).toEntity(Printer.noSpaces)) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.OK - response.asJson shouldEqual standardWriterMetadata(id, rev = rev + 1, schema1.id) + response.asJson shouldEqual standardWriterMetadata(id, rev = rev + 1, schema1) } } } @@ -341,7 +310,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with CatsIOValues { forAll(endpoints) { endpoint => Put(s"$endpoint", payloadUpdated.toEntity(Printer.noSpaces)) ~> asWriter ~> routes ~> check { status shouldEqual StatusCodes.OK - response.asJson shouldEqual standardWriterMetadata(id, rev = 1, schema = schema1.id) + response.asJson shouldEqual standardWriterMetadata(id, rev = 1, schema = schema1) } } } @@ -384,7 +353,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with CatsIOValues { givenAResourceWithSchema("myschema") { id => Put(s"/v1/resources/$projectRef/otherSchema/$id/update-schema") ~> asWriter ~> routes ~> check { response.status shouldEqual StatusCodes.OK - response.asJson.hcursor.get[String]("_constrainedBy").toOption should contain(schema3.id.toString) + response.asJson.hcursor.get[String]("_constrainedBy").toOption should contain(schema2.toString) } } } @@ -509,7 +478,7 @@ class ResourcesRoutesSpec extends BaseRouteSpec with CatsIOValues { s"/v1/resources/myorg/myproject/_/$id?rev=1", s"/v1/resources/myorg/myproject/$mySchema/$id?tag=$myTag" ) - val meta = standardWriterMetadata(id, schema = schema1.id, tpe = "schema:Custom") + val meta = standardWriterMetadata(id, schema = schema1, tpe = "schema:Custom") forAll(endpoints) { endpoint => Get(endpoint) ~> asReader ~> routes ~> check { status shouldEqual StatusCodes.OK diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesConfig.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesConfig.scala index e011e5fe83..d437b2dc0b 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesConfig.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesConfig.scala @@ -1,6 +1,8 @@ package ch.epfl.bluebrain.nexus.delta.sdk.resources +import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.sdk.resources.NexusSource.DecodingOption +import ch.epfl.bluebrain.nexus.delta.sdk.resources.ResourcesConfig.SchemaEnforcementConfig import ch.epfl.bluebrain.nexus.delta.sourcing.config.EventLogConfig import pureconfig.ConfigReader import pureconfig.generic.semiauto.deriveReader @@ -12,12 +14,31 @@ import pureconfig.generic.semiauto.deriveReader * configuration of the event log * @param decodingOption * strict/lenient decoding of resources + * @param schemaEnforcement + * configuration related to schema enforcement * @param skipUpdateNoChange * do not create a new revision when the update does not introduce a change in the current resource state */ -final case class ResourcesConfig(eventLog: EventLogConfig, decodingOption: DecodingOption, skipUpdateNoChange: Boolean) +final case class ResourcesConfig( + eventLog: EventLogConfig, + decodingOption: DecodingOption, + schemaEnforcement: SchemaEnforcementConfig, + skipUpdateNoChange: Boolean +) object ResourcesConfig { - implicit final val resourcesConfigReader: ConfigReader[ResourcesConfig] = + + /** + * Configuration to allow to bypass schema enforcing in some cases + * @param typeWhitelist + * types for which a schema is not required + * @param allowNoTypes + * allow to skip schema validation for resources without any types + */ + final case class SchemaEnforcementConfig(typeWhitelist: Set[Iri], allowNoTypes: Boolean) + + implicit final val resourcesConfigReader: ConfigReader[ResourcesConfig] = { + implicit val schemaEnforcementReader: ConfigReader[SchemaEnforcementConfig] = deriveReader[SchemaEnforcementConfig] deriveReader[ResourcesConfig] + } } diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/SchemaClaim.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/SchemaClaim.scala index 69d61195cb..df503d31bd 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/SchemaClaim.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/SchemaClaim.scala @@ -3,9 +3,6 @@ package ch.epfl.bluebrain.nexus.delta.sdk.resources import cats.effect.IO import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.schemas import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller -import ch.epfl.bluebrain.nexus.delta.sdk.resources.SchemaClaim._ -import ch.epfl.bluebrain.nexus.delta.sdk.resources.ValidationResult._ -import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection.SchemaIsMandatory import ch.epfl.bluebrain.nexus.delta.sourcing.model.{ProjectRef, ResourceRef} /** @@ -16,32 +13,8 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.{ProjectRef, ResourceRef} */ sealed trait SchemaClaim { - /** - * Validate the claim - * @param enforceSchema - * to ban unconstrained resources - * @param submitOnDefinedSchema - * the function to call when a schema is defined - */ - def validate(enforceSchema: Boolean)(submitOnDefinedSchema: SubmitOnDefinedSchema): IO[ValidationResult] = - this match { - case CreateWithSchema(project, schema, caller) => - submitOnDefinedSchema(project, schema, caller) - case CreateUnconstrained(project) => - onUnconstrained(project, enforceSchema) - case UpdateToSchema(project, schema, caller) => - submitOnDefinedSchema(project, schema, caller) - case UpdateToUnconstrained(project) => - onUnconstrained(project, enforceSchema) - case KeepUnconstrained(project) => - IO.pure(NoValidation(project)) - } - def project: ProjectRef - private def onUnconstrained(project: ProjectRef, enforceSchema: Boolean) = - IO.raiseWhen(enforceSchema)(SchemaIsMandatory(project)).as(NoValidation(project)) - } object SchemaClaim { @@ -52,16 +25,16 @@ object SchemaClaim { def schemaRef: ResourceRef } - final private case class CreateWithSchema(project: ProjectRef, schemaRef: ResourceRef, caller: Caller) + final case class CreateWithSchema(project: ProjectRef, schemaRef: ResourceRef, caller: Caller) extends DefinedSchemaClaim - final private case class CreateUnconstrained(project: ProjectRef) extends SchemaClaim + final case class CreateUnconstrained(project: ProjectRef) extends SchemaClaim - final private case class UpdateToSchema(project: ProjectRef, schemaRef: ResourceRef, caller: Caller) + final case class UpdateToSchema(project: ProjectRef, schemaRef: ResourceRef, caller: Caller) extends DefinedSchemaClaim - final private case class UpdateToUnconstrained(project: ProjectRef) extends SchemaClaim + final case class UpdateToUnconstrained(project: ProjectRef) extends SchemaClaim - final private case class KeepUnconstrained(project: ProjectRef) extends SchemaClaim + final case class KeepUnconstrained(project: ProjectRef) extends SchemaClaim private def isUnconstrained(schema: ResourceRef): Boolean = schema.iri == schemas.resources diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/SchemaClaimResolver.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/SchemaClaimResolver.scala new file mode 100644 index 0000000000..1cbd2e8cc2 --- /dev/null +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/SchemaClaimResolver.scala @@ -0,0 +1,74 @@ +package ch.epfl.bluebrain.nexus.delta.sdk.resources + +import cats.effect.IO +import cats.syntax.all._ +import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri +import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller +import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceF +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolution.ResourceResolution +import ch.epfl.bluebrain.nexus.delta.sdk.resources.ResourcesConfig.SchemaEnforcementConfig +import ch.epfl.bluebrain.nexus.delta.sdk.resources.SchemaClaim._ +import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection.{InvalidSchemaRejection, SchemaIsDeprecated, SchemaIsMandatory} +import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema +import ch.epfl.bluebrain.nexus.delta.sourcing.model.{ProjectRef, ResourceRef} + +trait SchemaClaimResolver { + + /** + * Resolves the schema claim to an actual schema + * @return + * the schema or none if unconstrained resources is allowed + */ + def apply(schemaClaim: SchemaClaim, resourceTypes: Set[Iri], enforceSchema: Boolean): IO[Option[ResourceF[Schema]]] + +} + +object SchemaClaimResolver { + + def apply( + resourceResolution: ResourceResolution[Schema], + schemaEnforcement: SchemaEnforcementConfig + ): SchemaClaimResolver = new SchemaClaimResolver { + override def apply( + schemaClaim: SchemaClaim, + resourceTypes: Set[Iri], + enforceSchema: Boolean + ): IO[Option[ResourceF[Schema]]] = + schemaClaim match { + case CreateWithSchema(project, schema, caller) => + resolveSchema(project, schema, caller) + case CreateUnconstrained(project) => + onUnconstrained(project, resourceTypes, enforceSchema) + case UpdateToSchema(project, schema, caller) => + resolveSchema(project, schema, caller) + case UpdateToUnconstrained(project) => + onUnconstrained(project, resourceTypes, enforceSchema) + case KeepUnconstrained(_) => + IO.none + } + + private def assertNotDeprecated(schema: ResourceF[Schema]) = { + IO.raiseWhen(schema.deprecated)(SchemaIsDeprecated(schema.value.id)) + } + + private def resolveSchema(project: ProjectRef, schema: ResourceRef, caller: Caller) = { + resourceResolution + .resolve(schema, project)(caller) + .flatMap { result => + val invalidSchema = result.leftMap(InvalidSchemaRejection(schema, project, _)) + IO.fromEither(invalidSchema) + } + .flatTap(schema => assertNotDeprecated(schema)) + .map(Some(_)) + } + + private def onUnconstrained(project: ProjectRef, resourceTypes: Set[Iri], enforceSchema: Boolean) = { + val enforcementRequired = enforceSchema && ( + resourceTypes.isEmpty && !schemaEnforcement.allowNoTypes || + resourceTypes.nonEmpty && !resourceTypes.forall(schemaEnforcement.typeWhitelist.contains) + ) + IO.raiseWhen(enforcementRequired)(SchemaIsMandatory(project)).as(None) + } + } + +} diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResource.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResource.scala index 59906ed58f..98f8264503 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResource.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResource.scala @@ -7,21 +7,19 @@ import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.contexts import ch.epfl.bluebrain.nexus.delta.rdf.shacl.{ValidateShacl, ValidationReport} -import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.JsonLdAssembly import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceF -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolution.ResourceResolution import ch.epfl.bluebrain.nexus.delta.sdk.resources.Resources.kamonComponent -import ch.epfl.bluebrain.nexus.delta.sdk.resources.SchemaClaim.SubmitOnDefinedSchema import ch.epfl.bluebrain.nexus.delta.sdk.resources.ValidationResult._ import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection._ import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema -import ch.epfl.bluebrain.nexus.delta.sourcing.model.{ProjectRef, ResourceRef} +import ch.epfl.bluebrain.nexus.delta.sourcing.model.ResourceRef /** * Allows to validate the resource: * - Validate it against the provided schema * - Checking if the provided resource id is not reserved + * - Checking if the provided resource types are not reserved */ trait ValidateResource { @@ -54,7 +52,7 @@ trait ValidateResource { object ValidateResource { def apply( - resourceResolution: ResourceResolution[Schema], + schemaClaimResolver: SchemaClaimResolver, validateShacl: ValidateShacl ): ValidateResource = new ValidateResource { @@ -63,10 +61,12 @@ object ValidateResource { schemaClaim: SchemaClaim, enforceSchema: Boolean ): IO[ValidationResult] = { - val submitOnDefinedSchema: SubmitOnDefinedSchema = resolveSchema(_, _, _).flatMap(apply(jsonld, _)) assertNotReservedId(jsonld.id) >> assertNotReservedTypes(jsonld.types) >> - schemaClaim.validate(enforceSchema)(submitOnDefinedSchema) + schemaClaimResolver(schemaClaim, jsonld.types, enforceSchema).flatMap { + case Some(schema) => apply(jsonld, schema) + case None => IO.pure(NoValidation(schemaClaim.project)) + } } def apply(jsonld: JsonLdAssembly, schema: ResourceF[Schema]): IO[ValidationResult] = { @@ -91,10 +91,6 @@ object ValidateResource { ResourceShaclEngineRejection(jsonld.id, schemaRef, e) }.span("validateShacl") - private def assertNotDeprecated(schema: ResourceF[Schema]) = { - IO.raiseWhen(schema.deprecated)(SchemaIsDeprecated(schema.value.id)) - } - private def assertNotReservedId(resourceId: Iri) = { IO.raiseWhen(resourceId.startsWith(contexts.base))(ReservedResourceId(resourceId)) } @@ -102,15 +98,5 @@ object ValidateResource { private def assertNotReservedTypes(types: Set[Iri]) = { IO.raiseWhen(types.exists(_.startsWith(Vocabulary.nxv.base)))(ReservedResourceTypes(types)) } - - private def resolveSchema(project: ProjectRef, schema: ResourceRef, caller: Caller) = { - resourceResolution - .resolve(schema, project)(caller) - .flatMap { result => - val invalidSchema = result.leftMap(InvalidSchemaRejection(schema, project, _)) - IO.fromEither(invalidSchema) - } - .flatTap(schema => assertNotDeprecated(schema)) - } } } diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesImplSpec.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesImplSpec.scala index 977c1f069f..5784e58d81 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesImplSpec.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ResourcesImplSpec.scala @@ -1,29 +1,22 @@ package ch.epfl.bluebrain.nexus.delta.sdk.resources -import cats.effect.IO import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.{contexts, nxv, schema, schemas} import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdJavaApi} import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.JsonLdContext.keywords import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteContextResolution} -import ch.epfl.bluebrain.nexus.delta.rdf.shacl.ValidateShacl -import ch.epfl.bluebrain.nexus.delta.sdk.generators.{ProjectGen, ResourceGen, ResourceResolutionGen, SchemaGen} +import ch.epfl.bluebrain.nexus.delta.sdk.generators.{ProjectGen, ResourceGen} import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.JsonLdRejection._ -import ch.epfl.bluebrain.nexus.delta.sdk.model.Fetch.FetchF import ch.epfl.bluebrain.nexus.delta.sdk.model.{IdSegment, IdSegmentRef, Tags} import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContextDummy import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ApiMappings import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ProjectRejection.{ProjectIsDeprecated, ProjectNotFound} import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolution.ResourceResolution -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport.ResolverReport -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{ResolverResolutionRejection, ResourceResolutionReport} -import ch.epfl.bluebrain.nexus.delta.sdk.resources.NexusSource.DecodingOption +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.Resource import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection._ -import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ import ch.epfl.bluebrain.nexus.delta.sdk.{ConfigFixtures, DataResource} import ch.epfl.bluebrain.nexus.delta.sourcing.ScopedEventLog @@ -41,6 +34,7 @@ import java.util.UUID class ResourcesImplSpec extends CatsEffectSpec with DoobieScalaTestFixture + with ValidateResourceFixture with CancelAfterFailure with ConfigFixtures with CirceLiteral { @@ -69,36 +63,26 @@ class ResourcesImplSpec private val projectRef = project.ref private val allApiMappings = am + Resources.mappings - private val schemaSource = jsonContentOf("resources/schema.json").addContext(contexts.shacl, contexts.schemasMetadata) - private val schema1 = SchemaGen.schema(nxv + "myschema", project.ref, schemaSource.removeKeys(keywords.id)) - private val schema2 = SchemaGen.schema(schema.Person, project.ref, schemaSource.removeKeys(keywords.id)) - private val schema3 = SchemaGen.schema(nxv + "myschema3", project.ref, schemaSource.removeKeys(keywords.id)) + private val schema1 = nxv + "myschema" + private val schema2 = nxv + "myschema2" - private val fetchSchema: (ResourceRef, ProjectRef) => FetchF[Schema] = { - case (ref, _) if ref.iri == schema2.id => IO.pure(Some(SchemaGen.resourceFor(schema2, deprecated = true))) - case (ref, _) if ref.iri == schema1.id => IO.pure(Some(SchemaGen.resourceFor(schema1))) - case (ref, _) if ref.iri == schema3.id => IO.pure(Some(SchemaGen.resourceFor(schema3))) - case _ => IO.none - } - private val resourceResolution: ResourceResolution[Schema] = - ResourceResolutionGen.singleInProject(projectRef, fetchSchema) + private val validateResource = validateFor(Set((project.ref, schema1), (project.ref, schema2))) - private val fetchContext = FetchContextDummy( + private val fetchContext = FetchContextDummy( Map( project.ref -> project.context.copy(apiMappings = allApiMappings), projectDeprecated.ref -> projectDeprecated.context ), Set(projectDeprecated.ref) ) - private val config = ResourcesConfig(eventLogConfig, DecodingOption.Strict, skipUpdateNoChange = true) - private val detectChanges = DetectChange(enabled = config.skipUpdateNoChange) + + private val detectChanges = DetectChange(enabled = true) private val resolverContextResolution: ResolverContextResolution = new ResolverContextResolution( rcr, (r, p, _) => resources.fetch(r, p).attempt.map(_.left.map(_ => ResourceResolutionReport())) ) - private val validateResource = ValidateResource(resourceResolution, ValidateShacl(rcr).accepted) private val resourceDef = Resources.definition(validateResource, detectChanges, clock) private lazy val resourceLog = ScopedEventLog(resourceDef, eventLogConfig, xas) @@ -140,7 +124,7 @@ class ResourcesImplSpec "creating a resource" should { "succeed with the id present on the payload" in { - forAll(List(myId -> resourceSchema, myId2 -> Latest(schema1.id))) { case (id, schemaRef) => + forAll(List(myId -> resourceSchema, myId2 -> Latest(schema1))) { case (id, schemaRef) => val sourceWithId = source deepMerge json"""{"@id": "$id"}""" val expectedData = ResourceGen.resource(id, projectRef, sourceWithId, Revision(schemaRef.iri, 1)) val resource = resources.create(projectRef, schemaRef, sourceWithId, None).accepted @@ -149,7 +133,7 @@ class ResourcesImplSpec } "succeed and tag with the id present on the payload" in { - forAll(List(myId10 -> resourceSchema, myId11 -> Latest(schema1.id))) { case (id, schemaRef) => + forAll(List(myId10 -> resourceSchema, myId11 -> Latest(schema1))) { case (id, schemaRef) => val sourceWithId = source deepMerge json"""{"@id": "$id"}""" val expectedData = ResourceGen.resource(id, projectRef, sourceWithId, Revision(schemaRef.iri, 1), Tags(tag -> 1)) @@ -167,7 +151,7 @@ class ResourcesImplSpec val list = List( (myId3, "_", resourceSchema), - (myId4, "myschema", Latest(schema1.id)) + (myId4, "myschema", Latest(schema1)) ) forAll(list) { case (id, schemaSegment, schemaRef) => val sourceWithId = source deepMerge json"""{"@id": "$id"}""" @@ -181,7 +165,7 @@ class ResourcesImplSpec val list = List( (myId12, "_", resourceSchema), - (myId13, "myschema", Latest(schema1.id)) + (myId13, "myschema", Latest(schema1)) ) forAll(list) { case (id, schemaSegment, schemaRef) => val sourceWithId = source deepMerge json"""{"@id": "$id"}""" @@ -200,7 +184,7 @@ class ResourcesImplSpec "succeed with the passed id" in { val list = List( ("nxv:myid5", myId5, resourceSchema), - ("nxv:myid6", myId6, Latest(schema1.id)) + ("nxv:myid6", myId6, Latest(schema1)) ) forAll(list) { case (segment, iri, schemaRef) => val sourceWithId = source deepMerge json"""{"@id": "$iri"}""" @@ -259,14 +243,6 @@ class ResourcesImplSpec resources.create(projectRef, schemas.resources, sourceWithBlankId, None).rejected shouldEqual BlankId } - "reject with ReservedResourceId" in { - forAll(List(Latest(schemas.resources), Latest(schema1.id))) { schemaRef => - val myId = contexts + "some.json" - val sourceWithReservedId = source deepMerge json"""{"@id": "$myId"}""" - resources.create(myId, projectRef, schemaRef, sourceWithReservedId, None).rejectedWith[ReservedResourceId] - } - } - "reject if it already exists" in { resources.create(myId, projectRef, schemas.resources, source, None).rejected shouldEqual ResourceAlreadyExists(myId, projectRef) @@ -277,22 +253,6 @@ class ResourcesImplSpec ResourceAlreadyExists(myId, projectRef) } - "reject if it does not validate against its schema" in { - val otherId = nxv + "other" - val wrongSource = source deepMerge json"""{"@id": "$otherId", "number": "wrong"}""" - resources.create(otherId, projectRef, schema1.id, wrongSource, None).rejectedWith[InvalidResource] - } - - "reject if the validated schema is deprecated" in { - val otherId = nxv + "other" - val noIdSource = source.removeKeys(keywords.id) - forAll(List[IdSegment](schema2.id, "Person")) { segment => - resources.create(otherId, projectRef, segment, noIdSource, None).rejected shouldEqual - SchemaIsDeprecated(schema2.id) - - } - } - "reject if the validated schema does not exists" in { val otherId = nxv + "other" val noIdSource = source.removeKeys(keywords.id) @@ -300,12 +260,7 @@ class ResourcesImplSpec InvalidSchemaRejection( Latest(nxv + "notExist"), project.ref, - ResourceResolutionReport( - ResolverReport.failed( - nxv + "in-project", - project.ref -> ResolverResolutionRejection.ResourceNotFound(nxv + "notExist", project.ref) - ) - ) + ResourceResolutionReport() ) } @@ -346,9 +301,9 @@ class ResourcesImplSpec "succeed" in { val updated = source.removeKeys(keywords.id) deepMerge json"""{"number": 60}""" - val expectedData = ResourceGen.resource(myId2, projectRef, updated, Revision(schema1.id, 1)) + val expectedData = ResourceGen.resource(myId2, projectRef, updated, Revision(schema1, 1)) val expected = mkResource(expectedData).copy(rev = 2) - val actual = resources.update(myId2, projectRef, Some(schema1.id), 1, updated, None).accepted + val actual = resources.update(myId2, projectRef, Some(schema1), 1, updated, None).accepted actual shouldEqual expected } @@ -356,9 +311,9 @@ class ResourcesImplSpec val updated = source.removeKeys(keywords.id) deepMerge json"""{"number": 60}""" val newTag = UserTag.unsafe(genString()) val expectedData = - ResourceGen.resource(myId10, projectRef, updated, Revision(schema1.id, 1), tags = Tags(tag -> 1, newTag -> 2)) + ResourceGen.resource(myId10, projectRef, updated, Revision(schema1, 1), tags = Tags(tag -> 1, newTag -> 2)) val expected = mkResource(expectedData).copy(rev = 2) - val actual = resources.update(myId10, projectRef, Some(schema1.id), 1, updated, Some(newTag)).accepted + val actual = resources.update(myId10, projectRef, Some(schema1), 1, updated, Some(newTag)).accepted val byTag = resources.fetch(IdSegmentRef(myId10, newTag), projectRef, None).accepted actual shouldEqual expected byTag shouldEqual expected @@ -366,14 +321,14 @@ class ResourcesImplSpec "succeed without specifying the schema" in { val updated = source.removeKeys(keywords.id) deepMerge json"""{"number": 65}""" - val expectedData = ResourceGen.resource(myId2, projectRef, updated, Revision(schema1.id, 1)) + val expectedData = ResourceGen.resource(myId2, projectRef, updated, Revision(schema1, 1)) resources.update("nxv:myid2", projectRef, None, 2, updated, None).accepted shouldEqual mkResource(expectedData).copy(rev = 3) } "succeed when changing the schema" in { val updatedSource = source.removeKeys(keywords.id) deepMerge json"""{"number": 70}""" - val newSchema = Revision(schema3.id, 1) + val newSchema = Revision(schema2, 1) val updatedResource = resources.update(myId2, projectRef, Some(newSchema.iri), 3, updatedSource, None).accepted updatedResource.rev shouldEqual 4 @@ -412,9 +367,10 @@ class ResourcesImplSpec .rejectedWith[ResourceIsDeprecated] } - "reject if it does not validate against its schema" in { - val wrongSource = source.removeKeys(keywords.id) deepMerge json"""{"number": "wrong"}""" - resources.update(myId2, projectRef, Some(schema1.id), 4, wrongSource, None).rejectedWith[InvalidResource] + "reject if the validated schema does not exists" in { + val otherId = nxv + "other" + val noIdSource = source.removeKeys(keywords.id) + resources.update(myId2, projectRef, Some(otherId), 4, noIdSource, None).rejectedWith[InvalidSchemaRejection] } "reject if project does not exist" in { @@ -467,7 +423,7 @@ class ResourcesImplSpec "reject if schemas do not match" in { resources - .refresh(idRefresh, projectRef, Some(schema1.id)) + .refresh(idRefresh, projectRef, Some(schema1)) .rejectedWith[UnexpectedResourceSchema] } @@ -503,12 +459,12 @@ class ResourcesImplSpec "reject if the resource doesn't exist" in { resources - .updateAttachedSchema(nonExistentResourceId, projectRef, schema3.id) + .updateAttachedSchema(nonExistentResourceId, projectRef, schema2) .rejectedWith[ResourceNotFound] } "reject if the schema doesn't exist" in { - resources.create(id, projectRef, schema1.id, sourceWithId, None).accepted + resources.create(id, projectRef, schema1, sourceWithId, None).accepted resources .updateAttachedSchema(id, projectRef, nonExistentSchemaId) .rejectedWith[InvalidSchemaRejection] @@ -516,16 +472,16 @@ class ResourcesImplSpec "reject if the project doesn't exist" in { resources - .updateAttachedSchema(id, nonExistentProject, schema3.id) + .updateAttachedSchema(id, nonExistentProject, schema2) .rejectedWith[ProjectNotFound] } "succeed" in { - val updated = resources.updateAttachedSchema(id, projectRef, schema3.id).accepted - updated.schema.iri shouldEqual schema3.id + val updated = resources.updateAttachedSchema(id, projectRef, schema2).accepted + updated.schema.iri shouldEqual schema2 val fetched = resources.fetch(id, projectRef, None).accepted - fetched.schema.iri shouldEqual schema3.id + fetched.schema.iri shouldEqual schema2 } } @@ -595,8 +551,8 @@ class ResourcesImplSpec "succeed" in { val sourceWithId = source deepMerge json"""{"@id": "$myId4"}""" - val expectedData = ResourceGen.resource(myId4, projectRef, sourceWithId, Revision(schema1.id, 1)) - val resource = resources.deprecate(myId4, projectRef, Some(schema1.id), 1).accepted + val expectedData = ResourceGen.resource(myId4, projectRef, sourceWithId, Revision(schema1, 1)) + val resource = resources.deprecate(myId4, projectRef, Some(schema1), 1).accepted resource shouldEqual mkResource(expectedData).copy(rev = 2, deprecated = true) assertDeprecated(myId4, projectRef) } @@ -623,7 +579,7 @@ class ResourcesImplSpec "reject if schemas do not match" in { givenAResource { id => - resources.deprecate(id, projectRef, Some(schema1.id), 1).assertRejectedWith[UnexpectedResourceSchema] + resources.deprecate(id, projectRef, Some(schema1), 1).assertRejectedWith[UnexpectedResourceSchema] assertRemainsActive(id, projectRef) } } @@ -680,7 +636,7 @@ class ResourcesImplSpec "reject if schema does not match" in { givenADeprecatedResource { id => resources - .undeprecate(id, projectRef, Some(schema1.id), 2) + .undeprecate(id, projectRef, Some(schema1), 2) .assertRejectedWith[UnexpectedResourceSchema] assertRemainsDeprecated(id, projectRef) } @@ -745,9 +701,9 @@ class ResourcesImplSpec } "fail fetching if schema is not resource schema" in { - resources.fetch(myId, projectRef, Some(schema1.id)).rejectedWith[ResourceNotFound] - resources.fetch(IdSegmentRef(myId, tag), projectRef, Some(schema1.id)).rejectedWith[ResourceNotFound] - resources.fetch(IdSegmentRef(myId, 2), projectRef, Some(schema1.id)).rejectedWith[ResourceNotFound] + resources.fetch(myId, projectRef, Some(schema1)).rejectedWith[ResourceNotFound] + resources.fetch(IdSegmentRef(myId, tag), projectRef, Some(schema1)).rejectedWith[ResourceNotFound] + resources.fetch(IdSegmentRef(myId, 2), projectRef, Some(schema1)).rejectedWith[ResourceNotFound] } "reject if project does not exist" in { diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/SchemaClaimResolverSuite.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/SchemaClaimResolverSuite.scala new file mode 100644 index 0000000000..e2aceeff6f --- /dev/null +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/SchemaClaimResolverSuite.scala @@ -0,0 +1,131 @@ +package ch.epfl.bluebrain.nexus.delta.sdk.resources + +import cats.effect.IO +import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri +import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.{contexts, nxv, schemas} +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdJavaApi} +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteContextResolution} +import ch.epfl.bluebrain.nexus.delta.sdk.generators.{ResourceResolutionGen, SchemaGen} +import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller +import ch.epfl.bluebrain.nexus.delta.sdk.model.Fetch.FetchF +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolution.ResourceResolution +import ch.epfl.bluebrain.nexus.delta.sdk.resources.ResourcesConfig.SchemaEnforcementConfig +import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection.{SchemaIsDeprecated, SchemaIsMandatory} +import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema +import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ +import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.User +import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef, ResourceRef} +import ch.epfl.bluebrain.nexus.testkit.mu.NexusSuite + +class SchemaClaimResolverSuite extends NexusSuite { + + implicit val api: JsonLdApi = JsonLdJavaApi.lenient + implicit private val rcr: RemoteContextResolution = + RemoteContextResolution.fixedIO( + contexts.metadata -> ContextValue.fromFile("contexts/metadata.json"), + contexts.shacl -> ContextValue.fromFile("contexts/shacl.json"), + contexts.schemasMetadata -> ContextValue.fromFile("contexts/schemas-metadata.json") + ) + + private val project = ProjectRef.unsafe("org", "proj") + + private val subject = User("myuser", Label.unsafe("myrealm")) + private val caller = Caller(subject, Set.empty) + + private def schemaValue(id: Iri) = loader + .jsonContentOf("resources/schema.json") + .map(_.addContext(contexts.shacl, contexts.schemasMetadata)) + .flatMap { source => + SchemaGen.schemaAsync(id, project, source) + } + .accepted + + private val unconstrained = ResourceRef.Revision(schemas.resources, 1) + + private val schemaId = nxv + "my-schema" + private val schemaRef = ResourceRef.Revision(schemaId, 1) + private val schema = SchemaGen.resourceFor(schemaValue(schemaId)) + + private val deprecatedSchemaId = nxv + "deprecated" + private val deprecatedSchemaRef = ResourceRef.Revision(deprecatedSchemaId, 1) + private val deprecatedSchema = SchemaGen.resourceFor(schemaValue(deprecatedSchemaId)).copy(deprecated = true) + + private val fetchSchema: (ResourceRef, ProjectRef) => FetchF[Schema] = { + case (ref, p) if ref.iri == schemaId && p == project => IO.some(schema) + case (ref, p) if ref.iri == deprecatedSchemaId && p == project => IO.some(deprecatedSchema) + case _ => IO.none + } + private val schemaResolution: ResourceResolution[Schema] = + ResourceResolutionGen.singleInProject(project, fetchSchema) + + private val strictSchemaClaimResolver = SchemaClaimResolver( + schemaResolution, + SchemaEnforcementConfig(Set.empty, allowNoTypes = false) + ) + + test(s"Succeed on a create claim with a defined schema with the strict resolver") { + val claim = SchemaClaim.onCreate(project, schemaRef, caller) + strictSchemaClaimResolver(claim, Set.empty, enforceSchema = true).assertEquals(Some(schema)) + } + + test(s"Succeed on a create claim without a schema and with no enforcement with the strict resolver") { + val claim = SchemaClaim.onCreate(project, unconstrained, caller) + strictSchemaClaimResolver(claim, Set.empty, enforceSchema = false).assertEquals(None) + } + + test("Fail on a create claim without a schema and with enforcement with the strict resolver") { + val claim = SchemaClaim.onCreate(project, unconstrained, caller) + strictSchemaClaimResolver(claim, Set.empty, enforceSchema = true).interceptEquals(SchemaIsMandatory(project)) + } + + test(s"Succeed on an update claim with a defined schema with the strict resolver") { + val claim = SchemaClaim.onUpdate(project, None, schemaRef, caller) + strictSchemaClaimResolver(claim, Set.empty, enforceSchema = true).assertEquals(Some(schema)) + } + + test(s"Succeed on an update claim when staying without a schema with the strict resolver") { + val claim = SchemaClaim.onUpdate(project, None, unconstrained, caller) + strictSchemaClaimResolver(claim, Set.empty, enforceSchema = true).assertEquals(None) + } + + test("Fail on a update claim without a schema and with enforcement with the strict resolver") { + val claim = SchemaClaim.onUpdate(project, Some(unconstrained), schemaRef, caller) + strictSchemaClaimResolver(claim, Set.empty, enforceSchema = true).interceptEquals(SchemaIsMandatory(project)) + } + + test("Fail on a create claim with a deprecated schema") { + val claim = SchemaClaim.onCreate(project, deprecatedSchemaRef, caller) + strictSchemaClaimResolver(claim, Set.empty, enforceSchema = false).interceptEquals( + SchemaIsDeprecated(deprecatedSchemaId) + ) + } + + private val whitelistedType = nxv + "Whitelist" + private val nonWhitelistedType = nxv + "NonWhitelist" + + private val lenientSchemaClaimResolver = SchemaClaimResolver( + schemaResolution, + SchemaEnforcementConfig(Set(whitelistedType), allowNoTypes = true) + ) + + test( + s"Succeed on a create claim without a schema and with enforcement on a whitelisted type with a lenient resolver" + ) { + val claim = SchemaClaim.onCreate(project, unconstrained, caller) + lenientSchemaClaimResolver(claim, Set(whitelistedType), enforceSchema = true).assertEquals(None) + } + + test(s"Succeed on a create claim without a schema and with enforcement for no type with a lenient resolver") { + val claim = SchemaClaim.onCreate(project, unconstrained, caller) + lenientSchemaClaimResolver(claim, Set.empty, enforceSchema = true).assertEquals(None) + } + + test( + s"Fail on a create claim without a schema and with enforcement on a non-whitelisted type with a lenient resolver" + ) { + val claim = SchemaClaim.onCreate(project, unconstrained, caller) + lenientSchemaClaimResolver(claim, Set(whitelistedType, nonWhitelistedType), enforceSchema = true).interceptEquals( + SchemaIsMandatory(project) + ) + } +} diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/SchemaClaimSuite.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/SchemaClaimSuite.scala deleted file mode 100644 index 5548118606..0000000000 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/SchemaClaimSuite.scala +++ /dev/null @@ -1,70 +0,0 @@ -package ch.epfl.bluebrain.nexus.delta.sdk.resources - -import cats.effect.IO -import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary -import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.nxv -import ch.epfl.bluebrain.nexus.delta.rdf.shacl.ValidationReport -import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller -import ch.epfl.bluebrain.nexus.delta.sdk.resources.SchemaClaim.SubmitOnDefinedSchema -import ch.epfl.bluebrain.nexus.delta.sdk.resources.ValidationResult._ -import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection.SchemaIsMandatory -import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.User -import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef, ResourceRef} -import ch.epfl.bluebrain.nexus.testkit.mu.NexusSuite -import io.circe.Json -import munit.Location - -class SchemaClaimSuite extends NexusSuite { - - private val project = ProjectRef.unsafe("org", "proj") - private val definedSchema = ResourceRef.Revision(nxv + "schema", 1) - private val unconstrained = ResourceRef.Revision(Vocabulary.schemas.resources, 1) - - private val subject = User("myuser", Label.unsafe("myrealm")) - private val caller = Caller(subject, Set.empty) - - private val noValidation = NoValidation(project) - private val validated = Validated(project, definedSchema, ValidationReport.unsafe(conforms = true, 42, Json.Null)) - - private val schemaIsMandatory = SchemaIsMandatory(project) - - private def submitOnDefinedSchema: SubmitOnDefinedSchema = (_, _, _) => IO.pure(validated) - - private def assertValidation(claim: SchemaClaim)(implicit location: Location) = - claim.validate(enforceSchema = true)(submitOnDefinedSchema).assertEquals(validated) >> - claim.validate(enforceSchema = false)(submitOnDefinedSchema).assertEquals(validated) - - private def assetNoValidation(claim: SchemaClaim)(implicit location: Location) = - claim.validate(enforceSchema = true)(submitOnDefinedSchema).assertEquals(noValidation) >> - claim.validate(enforceSchema = true)(submitOnDefinedSchema).assertEquals(noValidation) - - private def failWhenEnforceSchema(claim: SchemaClaim)(implicit location: Location) = - claim.validate(enforceSchema = true)(submitOnDefinedSchema).interceptEquals(schemaIsMandatory) >> - claim.validate(enforceSchema = false)(submitOnDefinedSchema).assertEquals(noValidation) - - test("Create with a schema runs validation") { - val claim = SchemaClaim.onCreate(project, definedSchema, caller) - assertValidation(claim) - } - - test("Create unconstrained fails or skip validation") { - val claim = SchemaClaim.onCreate(project, unconstrained, caller) - failWhenEnforceSchema(claim) - } - - test("Update to a defined schema runs validation") { - val claim = SchemaClaim.onUpdate(project, definedSchema, definedSchema, caller) - assertValidation(claim) - } - - test("Update to unconstrained fails or skip validation") { - val claim = SchemaClaim.onUpdate(project, unconstrained, definedSchema, caller) - failWhenEnforceSchema(claim) - } - - test("Keeping unconstrained skips validation") { - val claim = SchemaClaim.onUpdate(project, unconstrained, unconstrained, caller) - assetNoValidation(claim) - } - -} diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResourceFixture.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResourceFixture.scala index a05821f1cf..4cf42222d8 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResourceFixture.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResourceFixture.scala @@ -1,14 +1,17 @@ package ch.epfl.bluebrain.nexus.delta.sdk.resources import cats.effect.IO +import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.rdf.shacl.ValidationReport import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.JsonLdAssembly import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceF +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport import ch.epfl.bluebrain.nexus.delta.sdk.resources.ValidationResult._ import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection import ch.epfl.bluebrain.nexus.delta.sdk.resources.SchemaClaim.DefinedSchemaClaim +import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection.InvalidSchemaRejection import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema -import ch.epfl.bluebrain.nexus.delta.sourcing.model.ResourceRef +import ch.epfl.bluebrain.nexus.delta.sourcing.model.{ProjectRef, ResourceRef} import io.circe.Json import io.circe.syntax.KeyOps @@ -54,4 +57,31 @@ trait ValidateResourceFixture { ): IO[ValidationResult] = IO.raiseError(expected) } + def validateFor(validSchemas: Set[(ProjectRef, Iri)]): ValidateResource = + new ValidateResource { + override def apply(jsonld: JsonLdAssembly, schema: SchemaClaim, enforceSchema: Boolean): IO[ValidationResult] = { + val project = schema.project + schema match { + case defined: DefinedSchemaClaim if validSchemas.contains((project, defined.schemaRef.iri)) => + val schemaRevision = ResourceRef.Revision(defined.schemaRef.iri, defaultSchemaRevision) + IO.pure(Validated(project, schemaRevision, defaultReport)) + case defined: DefinedSchemaClaim => + IO.raiseError(InvalidSchemaRejection(defined.schemaRef, project, ResourceResolutionReport())) + case other => IO.pure(NoValidation(other.project)) + } + } + + override def apply( + jsonld: JsonLdAssembly, + schema: ResourceF[Schema] + ): IO[ValidationResult] = + IO.pure( + Validated( + schema.value.project, + ResourceRef.Revision(schema.id, schema.rev), + defaultReport + ) + ) + } + } diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResourceSuite.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResourceSuite.scala index 7e2db6c498..6fb93ada7a 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResourceSuite.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/resources/ValidateResourceSuite.scala @@ -14,6 +14,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.model.Fetch.FetchF import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverResolution.ResourceResolution import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.ResourceResolutionReport.ResolverReport import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.model.{ResolverResolutionRejection, ResourceResolutionReport} +import ch.epfl.bluebrain.nexus.delta.sdk.resources.ResourcesConfig.SchemaEnforcementConfig import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection._ import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema import ch.epfl.bluebrain.nexus.delta.sdk.syntax._ @@ -51,17 +52,11 @@ class ValidateResourceSuite extends NexusSuite { private val schema = schemaValue(schemaId) .map(SchemaGen.resourceFor(_)) - private val deprecatedSchemaId = nxv + "deprecated" - private val deprecatedSchemaRef = ResourceRef.Revision(deprecatedSchemaId, 1) - private val deprecatedSchema = schemaValue(deprecatedSchemaId) - .map(SchemaGen.resourceFor(_).copy(deprecated = true)) - private val unconstrained = ResourceRef.Revision(schemas.resources, 1) private val fetchSchema: (ResourceRef, ProjectRef) => FetchF[Schema] = { - case (ref, p) if ref.iri == schemaId && p == project => schema.map(Some(_)) - case (ref, p) if ref.iri == deprecatedSchemaId && p == project => deprecatedSchema.map(Some(_)) - case _ => IO.none + case (ref, p) if ref.iri == schemaId && p == project => schema.map(Some(_)) + case _ => IO.none } private val schemaResolution: ResourceResolution[Schema] = ResourceResolutionGen.singleInProject(project, fetchSchema) @@ -80,7 +75,10 @@ class ValidateResourceSuite extends NexusSuite { private val validResourceId = nxv + "valid" private val validResource = jsonLdWithId(validResourceId, identity) - private val validateResource = ValidateResource(schemaResolution, ValidateShacl(rcr).accepted) + private val schemaEnforcementConfig = SchemaEnforcementConfig(Set.empty, allowNoTypes = false) + private val schemaClaimResolver = SchemaClaimResolver(schemaResolution, schemaEnforcementConfig) + + private val validateResource = ValidateResource(schemaClaimResolver, ValidateShacl(rcr).accepted) private def assertResult(result: ValidationResult, expectedProject: ProjectRef, expectedSchema: ResourceRef.Revision)( implicit loc: Location @@ -157,16 +155,6 @@ class ValidateResourceSuite extends NexusSuite { } yield () } - test("Reject a resource when the resolved schema is deprecated") { - for { - jsonLd <- validResource - schemaClaim = SchemaClaim.onCreate(project, deprecatedSchemaRef, caller) - _ <- validateResource(jsonLd, schemaClaim, enforceSchema = true).interceptEquals( - SchemaIsDeprecated(deprecatedSchemaId) - ) - } yield () - } - test("Reject a resource when it can't be validated by the provided schema") { for { jsonLd <- jsonLdWithId(validResourceId, _.removeKeys("name")) diff --git a/ship/src/main/resources/ship-default.conf b/ship/src/main/resources/ship-default.conf index 44df41b28f..57ed78cdd5 100644 --- a/ship/src/main/resources/ship-default.conf +++ b/ship/src/main/resources/ship-default.conf @@ -78,9 +78,6 @@ ship { realm: "internal" } - # If true, no resource validation is performed - disable-resource-validation = false - # Specify the resource types that the ResourceProcessor should ignore resource-types-to-ignore = [] diff --git a/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/RunShip.scala b/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/RunShip.scala index a2800d24af..5bd7dda296 100644 --- a/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/RunShip.scala +++ b/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/RunShip.scala @@ -6,7 +6,6 @@ import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF import ch.epfl.bluebrain.nexus.delta.plugins.storage.FileSelf import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.operations.s3.client.S3StorageClient import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdJavaApi} -import ch.epfl.bluebrain.nexus.delta.rdf.shacl.ValidateShacl import ch.epfl.bluebrain.nexus.delta.sdk.organizations.FetchActiveOrganization import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContext import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ApiMappings @@ -54,9 +53,8 @@ object RunShip { // Wiring eventClock <- EventClock.init() remoteContextResolution <- ContextWiring.remoteContextResolution - validateShacl <- ValidateShacl(remoteContextResolution) (schemaLog, fetchSchema) = SchemaWiring(config.eventLog, eventClock, xas) - (resourceLog, fetchResource) = ResourceWiring(fetchContext, fetchSchema, validateShacl, config, eventClock, xas) + (resourceLog, fetchResource) = ResourceWiring(fetchSchema, config, eventClock, xas) rcr = ContextWiring.resolverContextResolution(fetchResource, fetchContext, remoteContextResolution, eventLogConfig, eventClock, xas) schemaImports = SchemaWiring.schemaImports(fetchResource, fetchSchema, fetchContext, eventLogConfig, eventClock, xas) // Processors diff --git a/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/config/InputConfig.scala b/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/config/InputConfig.scala index 6469627e0e..36d9a5d4d3 100644 --- a/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/config/InputConfig.scala +++ b/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/config/InputConfig.scala @@ -23,7 +23,6 @@ final case class InputConfig( storages: StoragesConfig, files: FileProcessingConfig, iriPatcher: IriPatcherConfig, - disableResourceValidation: Boolean, resourceTypesToIgnore: Set[Iri] ) diff --git a/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/resources/ResourceWiring.scala b/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/resources/ResourceWiring.scala index 4143516292..332590c46e 100644 --- a/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/resources/ResourceWiring.scala +++ b/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/resources/ResourceWiring.scala @@ -1,12 +1,9 @@ package ch.epfl.bluebrain.nexus.ship.resources import cats.effect.IO -import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.JsonLdApi -import ch.epfl.bluebrain.nexus.delta.rdf.shacl.{ValidateShacl, ValidationReport} +import ch.epfl.bluebrain.nexus.delta.rdf.shacl.ValidationReport import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.JsonLdAssembly import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceF -import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContext -import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResourceResolution import ch.epfl.bluebrain.nexus.delta.sdk.resources.Resources.ResourceLog import ch.epfl.bluebrain.nexus.delta.sdk.resources.SchemaClaim.DefinedSchemaClaim import ch.epfl.bluebrain.nexus.delta.sdk.resources.ValidationResult.{NoValidation, Validated} @@ -16,31 +13,21 @@ import ch.epfl.bluebrain.nexus.delta.sdk.schemas.model.Schema import ch.epfl.bluebrain.nexus.delta.sourcing.model.ResourceRef import ch.epfl.bluebrain.nexus.delta.sourcing.{ScopedEventLog, Transactors} import ch.epfl.bluebrain.nexus.ship.EventClock -import ch.epfl.bluebrain.nexus.ship.acls.AclWiring.alwaysAuthorize import ch.epfl.bluebrain.nexus.ship.config.InputConfig -import ch.epfl.bluebrain.nexus.ship.resolvers.ResolverWiring import io.circe.Json import io.circe.syntax.KeyOps object ResourceWiring { def apply( - fetchContext: FetchContext, fetchSchema: FetchSchema, - validateShacl: ValidateShacl, config: InputConfig, clock: EventClock, xas: Transactors - )(implicit - jsonLdApi: JsonLdApi ): (ResourceLog, FetchResource) = { - val detectChange = DetectChange(false) - val resolvers = ResolverWiring.resolvers(fetchContext, config.eventLog, clock, xas) - val resourceResolution = - ResourceResolution.schemaResource(alwaysAuthorize, resolvers, fetchSchema, excludeDeprecated = false) - val validate = ValidateResource(resourceResolution, validateShacl) + val detectChange = DetectChange(false) - val validation = if (config.disableResourceValidation) alwaysValidateResource(fetchSchema) else validate + val validation = alwaysValidateResource(fetchSchema) val resourceDef = Resources.definition(validation, detectChange, clock) val log = ScopedEventLog(resourceDef, config.eventLog, xas) diff --git a/ship/src/test/scala/ch/epfl/bluebrain/nexus/ship/config/ShipConfigFixtures.scala b/ship/src/test/scala/ch/epfl/bluebrain/nexus/ship/config/ShipConfigFixtures.scala index 949e894523..9ba8e24eb2 100644 --- a/ship/src/test/scala/ch/epfl/bluebrain/nexus/ship/config/ShipConfigFixtures.scala +++ b/ship/src/test/scala/ch/epfl/bluebrain/nexus/ship/config/ShipConfigFixtures.scala @@ -64,7 +64,6 @@ trait ShipConfigFixtures extends ConfigFixtures with StorageFixtures with Classp skipFileEvents = false ), IriPatcherConfig(enabled = false, iri"https://bbp.epfl.ch/", iri"https:/openbrainplatform.com/"), - disableResourceValidation = false, Set.empty ) diff --git a/tests/docker/config/delta-postgres.conf b/tests/docker/config/delta-postgres.conf index 7ebbfaeab8..ee1d1c65db 100644 --- a/tests/docker/config/delta-postgres.conf +++ b/tests/docker/config/delta-postgres.conf @@ -58,6 +58,13 @@ app { ttl = "3 days" } + resources { + schema-enforcement { + type-whitelist = ["http://schema.org/Chapter", "http://schema.org/Book"] + allow-no-types = true + } + } + service-account { subject: "service-account-delta" realm: "internal" diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/SearchConfigIndexingSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/SearchConfigIndexingSpec.scala index 7324774c37..8b834a5bf0 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/SearchConfigIndexingSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/kg/SearchConfigIndexingSpec.scala @@ -1089,14 +1089,13 @@ class SearchConfigIndexingSpec extends BaseIntegrationSpec { } "have simulationReady field true if curated" in { - val query = queryField(simulationReadymemodelId, "simulationReady") + val query = queryField(simulationReadymemodelId, "simulationReady") - assertOneSource(query) { json => - json shouldBe json"""{ "simulationReady": true }""" - } + assertOneSource(query) { json => + json shouldBe json"""{ "simulationReady": true }""" + } } - "have the correct single neuron simulation information" in { val expected = json"""{ diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/resources/EnforcedSchemaSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/resources/EnforcedSchemaSpec.scala index fafe56bedc..3f11bb53da 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/resources/EnforcedSchemaSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/resources/EnforcedSchemaSpec.scala @@ -78,8 +78,16 @@ class EnforcedSchemaSpec extends BaseIntegrationSpec { private def updateResourceSchema(id: String, schema: String) = deltaClient.put[Json](s"/resources/$project/${UrlUtils.encode(schema)}/$id/update-schema", Json.Null, Rick)(_) + private val contextPayload = json"""{ "@context": { "@base": "http://example.com/base/" } }""" + + private val whitelistedResourcePayload = json"""{ + "@context": { "schema" : "http://schema.org/" }, + "@type": ["schema:Book", "schema:Chapter"], + "schema:headline": "1984" + }""" + "Creating a resource" should { - "fail if no schema is provided" in { + "fail if no schema is provided for a non-whitelisted resource" in { for { payload <- SimpleResource.sourcePayload("xxx", 5) _ <- deltaClient.post[Json](s"/resources/$project/_/", payload, Rick) { failOnMissingSchema } @@ -94,6 +102,24 @@ class EnforcedSchemaSpec extends BaseIntegrationSpec { } } yield succeed } + + "succeed for a context (aka a resource with no type)" in { + deltaClient.post[Json](s"/resources/$project/_/", contextPayload, Rick) { expectCreated } + } + + "succeed for a resource where all types are whitelisted" in { + deltaClient.post[Json](s"/resources/$project/_/", whitelistedResourcePayload, Rick) { expectCreated } + } + + "fails for a resource where a type is not whitelisted" in { + val payload = + json"""{ + "@context": { "schema" : "http://schema.org/" }, + "@type": ["schema:Book", "schema:ComicStory"], + "schema:headline": "1984" + }""" + deltaClient.post[Json](s"/resources/$project/_/", payload, Rick) { failOnMissingSchema } + } } "Updating a validated resource" should { @@ -106,6 +132,20 @@ class EnforcedSchemaSpec extends BaseIntegrationSpec { } yield succeed } + "succeed when setting it as unconstrained for a whitelisted type" in { + for { + id <- createValidatedResource + _ <- updateResource(id, unconstrainedSchema, whitelistedResourcePayload) { expectOk } + } yield succeed + } + + "succeed when setting it as unconstrained for a context" in { + for { + id <- createValidatedResource + _ <- updateResource(id, unconstrainedSchema, contextPayload) { expectOk } + } yield succeed + } + "succeed when keeping the same schema" in { for { id <- createValidatedResource