diff --git a/.github/workflows/ci-delta-ship.yml b/.github/workflows/ci-delta-ship.yml index ecf8e997c5..30d134749a 100644 --- a/.github/workflows/ci-delta-ship.yml +++ b/.github/workflows/ci-delta-ship.yml @@ -35,7 +35,6 @@ jobs: - name: Clean, build Delta & Storage images run: | sbt -Dsbt.color=always -Dsbt.supershell=false \ - clean \ app/Docker/publishLocal - name: Start services run: docker-compose -f tests/docker/docker-compose.yml up -d @@ -46,4 +45,8 @@ jobs: - name: Unit tests run: | sbt -Dsbt.color=always -Dsbt.supershell=false \ - ship-unit-tests + "ship/testOnly *Suite" + - name: Integration tests + run: | + sbt -Dsbt.color=always -Dsbt.supershell=false \ + "ship/testOnly *Spec" diff --git a/build.sbt b/build.sbt index ecd6784a2e..a338abe72a 100755 --- a/build.sbt +++ b/build.sbt @@ -27,16 +27,16 @@ val byteBuddyAgentVersion = "1.10.17" val betterMonadicForVersion = "0.3.1" val caffeineVersion = "3.1.8" val catsEffectVersion = "3.5.4" -val catsRetryVersion = "3.1.0" +val catsRetryVersion = "3.1.3" val catsVersion = "2.10.0" val circeVersion = "0.14.6" val circeOpticsVersion = "0.15.0" val circeExtrasVersions = "0.14.3" val classgraphVersion = "4.8.168" val declineVersion = "2.4.1" -val distageVersion = "1.2.6" +val distageVersion = "1.2.7" val doobieVersion = "1.0.0-RC5" -val fs2Version = "3.9.4" +val fs2Version = "3.10.1" val googleAuthClientVersion = "1.35.0" val handleBarsVersion = "4.4.0" val hikariVersion = "5.1.0" @@ -52,7 +52,7 @@ val mockitoVersion = "1.17.30" val munitVersion = "1.0.0-M11" val munitCatsEffectVersion = "2.0.0-M4" val nimbusJoseJwtVersion = "9.37.3" -val postgresJdbcVersion = "42.7.2" +val postgresJdbcVersion = "42.7.3" val pureconfigVersion = "0.17.6" val scalaTestVersion = "3.2.18" val scalaXmlVersion = "2.2.0" @@ -729,16 +729,18 @@ lazy val delta = project lazy val ship = project .in(file("ship")) .settings( - name := "nexus-ship", - moduleName := "nexus-ship" + name := "nexus-ship", + moduleName := "nexus-ship", + Test / parallelExecution := false ) .enablePlugins(UniversalPlugin, JavaAppPackaging, JavaAgent, DockerPlugin, BuildInfoPlugin) .settings(shared, compilation, servicePackaging, assertJavaVersion, kamonSettings, coverage, release) .dependsOn( - sdk % "compile->compile;test->test", - blazegraphPlugin % "compile->compile", - elasticsearchPlugin % "compile->compile", - tests % "test->compile;test->test" + sdk % "compile->compile;test->test", + blazegraphPlugin % "compile->compile", + compositeViewsPlugin % "compile->compile", + elasticsearchPlugin % "compile->compile", + tests % "test->compile;test->test" ) .settings( libraryDependencies ++= Seq(declineEffect), diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutes.scala index 6986a47e99..dc0fd471bf 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutes.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/OrganizationsRoutes.scala @@ -3,7 +3,6 @@ package ch.epfl.bluebrain.nexus.delta.routes import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.server.{Directive1, Route} import cats.effect.IO -import cats.effect.unsafe.implicits._ import cats.implicits._ import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder @@ -12,8 +11,8 @@ import ch.epfl.bluebrain.nexus.delta.routes.OrganizationsRoutes.OrganizationInpu import ch.epfl.bluebrain.nexus.delta.sdk.OrganizationResource import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck import ch.epfl.bluebrain.nexus.delta.sdk.circe.CirceUnmarshalling -import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._ import ch.epfl.bluebrain.nexus.delta.sdk.directives.AuthDirectives +import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._ import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.implicits._ @@ -59,17 +58,15 @@ final class OrganizationsRoutes( import baseUri.prefixSegment private def orgsSearchParams(implicit caller: Caller): Directive1[OrganizationSearchParams] = - onSuccess(aclCheck.fetchAll.unsafeToFuture()).flatMap { allAcls => - (searchParams & parameter("label".?)).tmap { case (deprecated, rev, createdBy, updatedBy, label) => - OrganizationSearchParams( - deprecated, - rev, - createdBy, - updatedBy, - label, - org => aclCheck.authorizeFor(org.label, orgs.read, allAcls) - ) - } + (searchParams & parameter("label".?)).tmap { case (deprecated, rev, createdBy, updatedBy, label) => + OrganizationSearchParams( + deprecated, + rev, + createdBy, + updatedBy, + label, + org => aclCheck.authorizeFor(org.label, orgs.read) + ) } private def emitMetadata(value: IO[OrganizationResource]) = { diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ProjectsRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ProjectsRoutes.scala index c08a483971..fc875d97a6 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ProjectsRoutes.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ProjectsRoutes.scala @@ -60,18 +60,16 @@ final class ProjectsRoutes( implicit val paginationConfig: PaginationConfig = config.pagination private def projectsSearchParams(implicit caller: Caller): Directive1[ProjectSearchParams] = { - onSuccess(aclCheck.fetchAll.unsafeToFuture()).flatMap { allAcls => - (searchParams & parameter("label".?)).tmap { case (deprecated, rev, createdBy, updatedBy, label) => - ProjectSearchParams( - None, - deprecated, - rev, - createdBy, - updatedBy, - label, - proj => aclCheck.authorizeFor(proj.ref, ReadProjects, allAcls) - ) - } + (searchParams & parameter("label".?)).tmap { case (deprecated, rev, createdBy, updatedBy, label) => + ProjectSearchParams( + None, + deprecated, + rev, + createdBy, + updatedBy, + label, + proj => aclCheck.authorizeFor(proj.ref, ReadProjects) + ) } } diff --git a/delta/plugins/composite-views/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/CompositeViews.scala b/delta/plugins/composite-views/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/CompositeViews.scala index 2e83f1080b..2ed6a3209b 100644 --- a/delta/plugins/composite-views/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/CompositeViews.scala +++ b/delta/plugins/composite-views/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/CompositeViews.scala @@ -5,7 +5,6 @@ import ch.epfl.bluebrain.nexus.delta.kernel.kamon.KamonMetricComponent import ch.epfl.bluebrain.nexus.delta.kernel.search.Pagination.FromPagination import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF import ch.epfl.bluebrain.nexus.delta.plugins.compositeviews.CompositeViews._ -import ch.epfl.bluebrain.nexus.delta.plugins.compositeviews.config.CompositeViewsConfig import ch.epfl.bluebrain.nexus.delta.plugins.compositeviews.indexing.CompositeViewDef import ch.epfl.bluebrain.nexus.delta.plugins.compositeviews.indexing.CompositeViewDef.{ActiveViewDef, DeprecatedViewDef} import ch.epfl.bluebrain.nexus.delta.plugins.compositeviews.model.CompositeViewCommand._ @@ -27,6 +26,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.projects.{FetchContext, Projects} import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution import ch.epfl.bluebrain.nexus.delta.sdk.views.IndexingRev import ch.epfl.bluebrain.nexus.delta.sourcing._ +import ch.epfl.bluebrain.nexus.delta.sourcing.config.EventLogConfig import ch.epfl.bluebrain.nexus.delta.sourcing.model.EntityDependency.DependsOn import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject import ch.epfl.bluebrain.nexus.delta.sourcing.model._ @@ -34,6 +34,8 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.offset.Offset import ch.epfl.bluebrain.nexus.delta.sourcing.stream.Elem import io.circe.Json +import scala.concurrent.duration.FiniteDuration + /** * Composite views resource lifecycle operations. */ @@ -467,7 +469,8 @@ object CompositeViews { fetchContext: FetchContext, contextResolution: ResolverContextResolution, validate: ValidateCompositeView, - config: CompositeViewsConfig, + minIntervalRebuild: FiniteDuration, + eventLogConfig: EventLogConfig, xas: Transactors, clock: Clock[IO] )(implicit @@ -476,13 +479,13 @@ object CompositeViews { ): IO[CompositeViews] = IO .delay( - CompositeViewFieldsJsonLdSourceDecoder(uuidF, contextResolution, config.minIntervalRebuild) + CompositeViewFieldsJsonLdSourceDecoder(uuidF, contextResolution, minIntervalRebuild) ) .map { sourceDecoder => new CompositeViews( ScopedEventLog( definition(validate, clock), - config.eventLog, + eventLogConfig, xas ), fetchContext, diff --git a/delta/plugins/composite-views/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/CompositeViewsPluginModule.scala b/delta/plugins/composite-views/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/CompositeViewsPluginModule.scala index 5a787cd83d..760f1fcc1e 100644 --- a/delta/plugins/composite-views/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/CompositeViewsPluginModule.scala +++ b/delta/plugins/composite-views/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/CompositeViewsPluginModule.scala @@ -129,7 +129,8 @@ class CompositeViewsPluginModule(priority: Int) extends ModuleDef { fetchContext, contextResolution, validate, - config, + config.minIntervalRebuild, + config.eventLog, xas, clock )( diff --git a/delta/plugins/composite-views/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/model/CompositeViewFields.scala b/delta/plugins/composite-views/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/model/CompositeViewFields.scala index e177e0c029..31e1a66cd9 100644 --- a/delta/plugins/composite-views/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/model/CompositeViewFields.scala +++ b/delta/plugins/composite-views/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/model/CompositeViewFields.scala @@ -4,9 +4,11 @@ import cats.data.NonEmptyList import ch.epfl.bluebrain.nexus.delta.plugins.compositeviews.model.CompositeView.RebuildStrategy import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.JsonLdContext.keywords -import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.decoder.JsonLdDecoder +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.decoder.{Configuration, JsonLdDecoder} import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.decoder.JsonLdDecoderError.ParsingFailure +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.decoder.configuration.semiauto.deriveConfigJsonLdDecoder import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.decoder.semiauto.deriveDefaultJsonLdDecoder +import ch.epfl.bluebrain.nexus.delta.rdf.syntax.iriStringContextSyntax import ch.epfl.bluebrain.nexus.delta.sdk.model.BaseUri import io.circe.syntax.EncoderOps import io.circe.{Encoder, Json} @@ -73,6 +75,12 @@ object CompositeViewFields { } deriveDefaultJsonLdDecoder[RebuildStrategy] } - deriveDefaultJsonLdDecoder[CompositeViewFields] + + val ctx = Configuration.default.context + .addAliasIdType("description", iri"http://schema.org/description") + .addAliasIdType("name", iri"http://schema.org/name") + implicit val config = Configuration.default.copy(context = ctx) + + deriveConfigJsonLdDecoder[CompositeViewFields] } } diff --git a/delta/plugins/composite-views/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/CompositeViewsSpec.scala b/delta/plugins/composite-views/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/CompositeViewsSpec.scala index 5beb5f54e9..b15b086f7b 100644 --- a/delta/plugins/composite-views/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/CompositeViewsSpec.scala +++ b/delta/plugins/composite-views/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/CompositeViewsSpec.scala @@ -52,7 +52,8 @@ class CompositeViewsSpec fetchContext, ResolverContextResolution(rcr), alwaysValidate, - config, + config.minIntervalRebuild, + config.eventLog, xas, clock ).accepted diff --git a/delta/plugins/composite-views/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/routes/CompositeViewsRoutesSpec.scala b/delta/plugins/composite-views/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/routes/CompositeViewsRoutesSpec.scala index 896d1680f7..ca1bd7b49e 100644 --- a/delta/plugins/composite-views/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/routes/CompositeViewsRoutesSpec.scala +++ b/delta/plugins/composite-views/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/compositeviews/routes/CompositeViewsRoutesSpec.scala @@ -50,7 +50,8 @@ class CompositeViewsRoutesSpec extends CompositeViewsRoutesFixtures { fetchContext, ResolverContextResolution(rcr), alwaysValidate, - config, + config.minIntervalRebuild, + config.eventLog, xas, clock ).accepted diff --git a/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/client/ElasticSearchClientSpec.scala b/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/client/ElasticSearchClientSpec.scala index 0796c0030c..9765471399 100644 --- a/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/client/ElasticSearchClientSpec.scala +++ b/delta/plugins/elasticsearch/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/elasticsearch/client/ElasticSearchClientSpec.scala @@ -4,6 +4,7 @@ import akka.actor.ActorSystem import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model.Uri.Query import akka.testkit.TestKit +import cats.effect.IO import cats.implicits._ import ch.epfl.bluebrain.nexus.delta.kernel.search.Pagination.FromPagination import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.ScalaTestElasticSearchClientSetup @@ -23,7 +24,7 @@ import ch.epfl.bluebrain.nexus.testkit.CirceLiteral import ch.epfl.bluebrain.nexus.testkit.elasticsearch.ElasticSearchDocker import ch.epfl.bluebrain.nexus.testkit.scalatest.ce.CatsEffectSpec import io.circe.{Json, JsonObject} -import org.scalatest.DoNotDiscover +import org.scalatest.{Assertion, DoNotDiscover} import org.scalatest.concurrent.Eventually import scala.concurrent.duration._ @@ -260,23 +261,25 @@ class ElasticSearchClientSpec(override val docker: ElasticSearchDocker) ElasticSearchAction.Update(index, "1", json"""{ "doc" : {"field2" : "value2"} }""") ) + def theCountShouldBe(count: Long): IO[Assertion] = + esClient.count(index.value).map(_ shouldEqual count) + { for { // Indexing and checking count - _ <- esClient.bulk(operations) - _ <- esClient.refresh(index) - original <- esClient.count(index.value) - _ = original shouldEqual 2L + _ <- esClient.bulk(operations) + _ <- esClient.refresh(index) + _ <- eventually { theCountShouldBe(2L) } // Deleting document matching the given query - query = jobj"""{"query": {"bool": {"must": {"term": {"field1": 3} } } } }""" - _ <- esClient.deleteByQuery(query, index) + query = jobj"""{"query": {"bool": {"must": {"term": {"field1": 3} } } } }""" + _ <- esClient.deleteByQuery(query, index) // Checking docs again - newCount <- esClient.count(index.value) - _ = newCount shouldEqual 1L - doc1 <- esClient.getSource[Json](index, "1").attemptNarrow[HttpClientError] - _ = doc1.rightValue - doc2 <- esClient.getSource[Json](index, "2").attemptNarrow[HttpClientError] - _ = doc2.leftValue.errorCode.value shouldEqual StatusCodes.NotFound + _ <- esClient.refresh(index) + _ <- eventually { theCountShouldBe(1L) } + doc1 <- esClient.getSource[Json](index, "1").attemptNarrow[HttpClientError] + _ = doc1.rightValue + doc2 <- esClient.getSource[Json](index, "2").attemptNarrow[HttpClientError] + _ = doc2.leftValue.errorCode.value shouldEqual StatusCodes.NotFound } yield () }.accepted } diff --git a/delta/plugins/search/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/search/SearchScopeInitializationSpec.scala b/delta/plugins/search/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/search/SearchScopeInitializationSpec.scala index 0ead8e8e9c..875cdc7241 100644 --- a/delta/plugins/search/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/search/SearchScopeInitializationSpec.scala +++ b/delta/plugins/search/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/search/SearchScopeInitializationSpec.scala @@ -35,7 +35,8 @@ class SearchScopeInitializationSpec fetchContext, ResolverContextResolution(rcr), alwaysValidate, - config, + config.minIntervalRebuild, + config.eventLog, xas, clock ).accepted diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/acls/AclCheck.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/acls/AclCheck.scala index d6f18b886e..dacf2a35e3 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/acls/AclCheck.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/acls/AclCheck.scala @@ -1,7 +1,7 @@ package ch.epfl.bluebrain.nexus.delta.sdk.acls -import cats.effect.IO -import cats.syntax.all._ +import cats.effect.{IO, Ref} +import cats.implicits.toFoldableOps import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.AclAddressFilter.AnyOrganizationAnyProject import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.AclRejection.AclNotFound import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.{Acl, AclAddress} @@ -10,36 +10,16 @@ import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity import scala.collection.immutable.Iterable -import cats.effect.Ref -/** - * Check authorizations on acls - */ trait AclCheck { - def fetchOne: AclAddress => IO[Acl] - - def fetchAll: IO[Map[AclAddress, Acl]] - - private def authorizeForOrFail[E <: Throwable]( - path: AclAddress, - permission: Permission, - identities: Set[Identity], - f: AclAddress => IO[Acl] - )( - onError: => E - ): IO[Unit] = - authorizeFor(path, permission, identities, f) - .flatMap { result => IO.raiseWhen(!result)(onError) } - /** * Checks whether the provided entities has the passed ''permission'' on the passed ''path'', raising the error * ''onError'' when it doesn't */ def authorizeForOr[E <: Throwable](path: AclAddress, permission: Permission, identities: Set[Identity])( onError: => E - ): IO[Unit] = - authorizeForOrFail(path, permission, identities, fetchOne)(onError) + ): IO[Unit] /** * Checks whether a given [[Caller]] has the passed ''permission'' on the passed ''path'', raising the error @@ -53,35 +33,7 @@ trait AclCheck { /** * Checks whether the provided entities have the passed ''permission'' on the passed ''path''. */ - def authorizeFor( - path: AclAddress, - permission: Permission, - identities: Set[Identity], - f: AclAddress => IO[Acl] - ): IO[Boolean] = - path.ancestors - .foldM(false) { - case (false, address) => f(address).redeem(_ => false, _.hasPermission(identities, permission)) - case (true, _) => IO.pure(true) - } - - /** - * Checkswhether a given [[Caller]] has the passed ''permission'' on the passed ''path''. - */ - def authorizeFor( - path: AclAddress, - permission: Permission, - acls: Map[AclAddress, Acl] - )(implicit caller: Caller): IO[Boolean] = { - def fetch = (address: AclAddress) => IO.fromOption(acls.get(address))(AclNotFound(address)) - authorizeFor(path, permission, caller.identities, fetch) - } - - /** - * Checks whether the provided entities have the passed ''permission'' on the passed ''path''. - */ - def authorizeFor(path: AclAddress, permission: Permission, identities: Set[Identity]): IO[Boolean] = - authorizeFor(path, permission, identities, fetchOne) + def authorizeFor(path: AclAddress, permission: Permission, identities: Set[Identity]): IO[Boolean] /** * Checks whether a given [[Caller]] has the passed ''permission'' on the passed ''path''. @@ -95,34 +47,13 @@ trait AclCheck { */ def authorizeForEveryOr[E <: Throwable](path: AclAddress, permissions: Set[Permission])( onError: => E - )(implicit caller: Caller): IO[Unit] = - path.ancestors - .foldM((false, Set.empty[Permission])) { - case ((true, set), _) => IO.pure((true, set)) - case ((false, set), address) => - fetchOne(address) - .redeem( - _ => (false, set), - { res => - val resSet = - res.value // fetch the set of permissions defined in the resource that apply to the caller - .filter { case (identity, _) => caller.identities.contains(identity) } - .values - .foldLeft(Set.empty[Permission])(_ ++ _) - val sum = set ++ resSet // add it to the accumulated set - (permissions.forall(sum.contains), sum) // true if all permissions are found, recurse otherwise - } - ) - } - .flatMap { case (result, _) => IO.raiseWhen(!result)(onError) } + )(implicit caller: Caller): IO[Unit] /** * Map authorized values for the provided caller. * - * Will raise an error [[E]] at the first unauthorized attempt - * * @param values - * the list of couples addres permission to check + * the list of couples address permission to check * @param extractAddressPermission * Extract an acl address and permission from a value [[A]] * @param onAuthorized @@ -130,25 +61,15 @@ trait AclCheck { * @param onFailure * to raise an error at the first unauthorized value */ - def mapFilterOrRaise[E, A, B]( + def mapFilterOrRaise[A, B]( values: Iterable[A], extractAddressPermission: A => (AclAddress, Permission), onAuthorized: A => B, onFailure: AclAddress => IO[Unit] - )(implicit caller: Caller): IO[Set[B]] = fetchAll.flatMap { allAcls => - values.toList.foldLeftM(Set.empty[B]) { case (acc, value) => - val (address, permission) = extractAddressPermission(value) - authorizeFor(address, permission, allAcls).flatMap { success => - if (success) - IO.pure(acc + onAuthorized(value)) - else - onFailure(address) >> IO.pure(acc) - } - } - } + )(implicit caller: Caller): IO[Set[B]] /** - * Map authorized values for the provided caller while fitering out the unauthorized ones. + * Map authorized values for the provided caller while filtering out the unauthorized ones. * * @param values * the values to work on @@ -165,51 +86,30 @@ trait AclCheck { mapFilterOrRaise(values, extractAddressPermission, onAuthorized, _ => IO.unit) /** - * Map authorized values for the provided caller. - * - * Will raise an error [[E]] at the first unauthorized attempt + * Map authorized values for the provided caller while filtering out the unauthorized ones. * * @param values - * the list of couples addres permission to check + * the list of couples address permission to check * @param address * the address to check for * @param extractPermission * Extract an acl address and permission from a value [[A]] * @param onAuthorized * to map the value [[A]] to [[B]] if access is granted - * @param onFailure - * to raise an error at the first unauthorized value */ - def mapFilterAtAddressOrRaise[E, A, B]( + def mapFilterAtAddressOrRaise[A, B]( values: Iterable[A], address: AclAddress, extractPermission: A => Permission, onAuthorized: A => B, onFailure: AclAddress => IO[Unit] - )(implicit caller: Caller): IO[Set[B]] = - Ref.of[IO, Map[AclAddress, Acl]](Map.empty).flatMap { cache => - def fetch: AclAddress => IO[Acl] = (address: AclAddress) => - cache.get.map(_.get(address)).flatMap { - case Some(acl) => IO.pure(acl) - case None => fetchOne(address).flatTap { acl => cache.update { _.updated(address, acl) } } - } - - values.toList.foldLeftM(Set.empty[B]) { case (acc, value) => - val permission = extractPermission(value) - authorizeFor(address, permission, caller.identities, fetch).flatMap { success => - if (success) - IO.pure(acc + onAuthorized(value)) - else - onFailure(address) >> IO.pure(acc) - } - } - } + )(implicit caller: Caller): IO[Set[B]] /** - * Map authorized values for the provided caller while fitering out the unauthorized ones. + * Map authorized values for the provided caller while filtering out the unauthorized ones. * * @param values - * the list of couples addres permission to check + * the list of couples address permission to check * @param address * the address to check for * @param extractPermission @@ -224,17 +124,133 @@ trait AclCheck { onAuthorized: A => B )(implicit caller: Caller): IO[Set[B]] = mapFilterAtAddressOrRaise(values, address, extractPermission, onAuthorized, _ => IO.unit) + } object AclCheck { - def apply(acls: Acls): AclCheck = new AclCheck { - override def fetchOne: AclAddress => IO[Acl] = acls.fetch(_).map(_.value) - - override def fetchAll: IO[Map[AclAddress, Acl]] = + def apply(acls: Acls): AclCheck = + apply( + acls.fetch(_).map(_.value), acls .list(AnyOrganizationAnyProject(true)) .map(_.value.map { case (address, resource) => address -> resource.value }) + ) + + def apply( + fetchOne: AclAddress => IO[Acl], + fetchAll: IO[Map[AclAddress, Acl]] + ): AclCheck = new AclCheck { + + def authorizeForOrFail[E <: Throwable]( + path: AclAddress, + permission: Permission, + identities: Set[Identity], + f: AclAddress => IO[Acl] + )( + onError: => E + ): IO[Unit] = + authorizeFor(path, permission, identities, f) + .flatMap { result => IO.raiseWhen(!result)(onError) } + + /** + * Checks whether the provided entities have the passed ''permission'' on the passed ''path''. + */ + def authorizeFor( + path: AclAddress, + permission: Permission, + identities: Set[Identity], + f: AclAddress => IO[Acl] + ): IO[Boolean] = + path.ancestors + .foldM(false) { + case (false, address) => f(address).redeem(_ => false, _.hasPermission(identities, permission)) + case (true, _) => IO.pure(true) + } + + /** + * Checkswhether a given [[Caller]] has the passed ''permission'' on the passed ''path''. + */ + def authorizeFor( + path: AclAddress, + permission: Permission, + acls: Map[AclAddress, Acl] + )(implicit caller: Caller): IO[Boolean] = { + def fetch = (address: AclAddress) => IO.fromOption(acls.get(address))(AclNotFound(address)) + authorizeFor(path, permission, caller.identities, fetch) + } + + override def authorizeForOr[E <: Throwable](path: AclAddress, permission: Permission, identities: Set[Identity])( + onError: => E + ): IO[Unit] = authorizeForOrFail(path, permission, identities, fetchOne)(onError) + + override def authorizeFor(path: AclAddress, permission: Permission, identities: Set[Identity]): IO[Boolean] = + authorizeFor(path, permission, identities, fetchOne) + + override def authorizeForEveryOr[E <: Throwable](path: AclAddress, permissions: Set[Permission])(onError: => E)( + implicit caller: Caller + ): IO[Unit] = + path.ancestors + .foldM((false, Set.empty[Permission])) { + case ((true, set), _) => IO.pure((true, set)) + case ((false, set), address) => + fetchOne(address) + .redeem( + _ => (false, set), + { res => + val resSet = + res.value // fetch the set of permissions defined in the resource that apply to the caller + .filter { case (identity, _) => caller.identities.contains(identity) } + .values + .foldLeft(Set.empty[Permission])(_ ++ _) + val sum = set ++ resSet // add it to the accumulated set + (permissions.forall(sum.contains), sum) // true if all permissions are found, recurse otherwise + } + ) + } + .flatMap { case (result, _) => IO.raiseWhen(!result)(onError) } + + override def mapFilterOrRaise[A, B]( + values: Iterable[A], + extractAddressPermission: A => (AclAddress, Permission), + onAuthorized: A => B, + onFailure: AclAddress => IO[Unit] + )(implicit caller: Caller): IO[Set[B]] = fetchAll.flatMap { allAcls => + values.toList.foldLeftM(Set.empty[B]) { case (acc, value) => + val (address, permission) = extractAddressPermission(value) + authorizeFor(address, permission, allAcls).flatMap { success => + if (success) + IO.pure(acc + onAuthorized(value)) + else + onFailure(address) >> IO.pure(acc) + } + } + } + + def mapFilterAtAddressOrRaise[A, B]( + values: Iterable[A], + address: AclAddress, + extractPermission: A => Permission, + onAuthorized: A => B, + onFailure: AclAddress => IO[Unit] + )(implicit caller: Caller): IO[Set[B]] = + Ref.of[IO, Map[AclAddress, Acl]](Map.empty).flatMap { cache => + def fetch: AclAddress => IO[Acl] = (address: AclAddress) => + cache.get.map(_.get(address)).flatMap { + case Some(acl) => IO.pure(acl) + case None => fetchOne(address).flatTap { acl => cache.update { _.updated(address, acl) } } + } + + values.toList.foldLeftM(Set.empty[B]) { case (acc, value) => + val permission = extractPermission(value) + authorizeFor(address, permission, caller.identities, fetch).flatMap { success => + if (success) + IO.pure(acc + onAuthorized(value)) + else + onFailure(address) >> IO.pure(acc) + } + } + } } } diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/multifetch/MultiFetch.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/multifetch/MultiFetch.scala index 438e446b21..a801698433 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/multifetch/MultiFetch.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/multifetch/MultiFetch.scala @@ -28,10 +28,10 @@ object MultiFetch { new MultiFetch { override def apply(request: MultiFetchRequest)(implicit caller: Caller - ): IO[MultiFetchResponse] = aclCheck.fetchAll.flatMap { allAcls => + ): IO[MultiFetchResponse] = request.resources .traverse { input => - aclCheck.authorizeFor(input.project, resources.read, allAcls).flatMap { + aclCheck.authorizeFor(input.project, resources.read).flatMap { case true => fetchResource(input).map { _.map(Success(input.id, input.project, _)) @@ -44,7 +44,6 @@ object MultiFetch { .map { resources => MultiFetchResponse(request.format, resources) } - } } } diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/acls/AclCheckSuite.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/acls/AclCheckSuite.scala index 94815c12de..ccb5e26058 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/acls/AclCheckSuite.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/acls/AclCheckSuite.scala @@ -2,7 +2,7 @@ package ch.epfl.bluebrain.nexus.delta.sdk.acls import cats.effect.IO import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheckSuite.{ProjectValue, Value} -import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.{Acl, AclAddress} +import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.AclAddress import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions._ import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission @@ -33,16 +33,10 @@ class AclCheckSuite extends NexusSuite { private val unauthorizedError = new IllegalArgumentException("The user has no access to this resource.") test("Return the acls provided at initialization") { - aclCheck.fetchAll.assertEquals( - Map( - AclAddress.Root -> Acl(AclAddress.Root, Anonymous -> Set(events.read)), - AclAddress.Organization(org1) -> Acl( - AclAddress.Organization(org1), - alice.subject -> Set(resources.read, resources.write) - ), - AclAddress.Project(proj11) -> Acl(AclAddress.Project(proj11), bob.subject -> Set(resources.read)) - ) - ) + aclCheck.authorizeFor(AclAddress.Root, events.read, Set(Anonymous)).assertEquals(true) >> + aclCheck.authorizeFor(AclAddress.Organization(org1), resources.read, Set(aliceUser)).assertEquals(true) >> + aclCheck.authorizeFor(AclAddress.Organization(org1), resources.write, Set(aliceUser)).assertEquals(true) >> + aclCheck.authorizeFor(AclAddress.Project(proj11), resources.read, Set(bobUser)).assertEquals(true) } List(alice, bob).foreach { caller => @@ -104,7 +98,7 @@ class AclCheckSuite extends NexusSuite { test("Map and filter a list of values for the user Alice without raising an error") { aclCheck - .mapFilterOrRaise[String, ProjectValue, Int]( + .mapFilterOrRaise[ProjectValue, Int]( projectValues, v => (v.project, v.permission), _.index, @@ -125,7 +119,7 @@ class AclCheckSuite extends NexusSuite { test("Raise an error as bob is missing some of the acls") { aclCheck - .mapFilterOrRaise[String, ProjectValue, Int]( + .mapFilterOrRaise[ProjectValue, Int]( projectValues, v => (v.project, v.permission), _.index, @@ -153,7 +147,7 @@ class AclCheckSuite extends NexusSuite { test("Map and filter a list of values at a given address for the user Alice without raising an error") { aclCheck - .mapFilterAtAddressOrRaise[String, Value, Int]( + .mapFilterAtAddressOrRaise[Value, Int]( values, proj12, _.permission, @@ -176,7 +170,7 @@ class AclCheckSuite extends NexusSuite { test("Raise an error for values at a given address as bob is missing some of the acls") { aclCheck - .mapFilterAtAddressOrRaise[String, Value, Int]( + .mapFilterAtAddressOrRaise[Value, Int]( values, proj11, _.permission, diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/acls/AclSimpleCheck.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/acls/AclSimpleCheck.scala index effd09fd58..1111703e85 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/acls/AclSimpleCheck.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/acls/AclSimpleCheck.scala @@ -1,18 +1,20 @@ package ch.epfl.bluebrain.nexus.delta.sdk.acls -import cats.effect.IO +import cats.effect.unsafe.implicits._ +import cats.effect.{IO, Ref} import cats.syntax.all._ import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.AclRejection.AclNotFound import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.{Acl, AclAddress} +import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity -import cats.effect.Ref -import cats.effect.unsafe.implicits._ + +import scala.collection.immutable /** * In-memory implementation of an [[AclCheck]] */ -final class AclSimpleCheck private (cache: Ref[IO, Map[AclAddress, Acl]]) extends AclCheck { +abstract class AclSimpleCheck private (cache: Ref[IO, Map[AclAddress, Acl]]) extends AclCheck { def append(acl: Acl): IO[Unit] = cache.updateAndGet { c => @@ -33,31 +35,67 @@ final class AclSimpleCheck private (cache: Ref[IO, Map[AclAddress, Acl]]) extend val newAcl = Acl(address, acl.toMap) c.updatedWith(address)(_.map(_ -- newAcl).orElse(Some(newAcl))) }.void - - override def fetchOne: AclAddress => IO[Acl] = (address: AclAddress) => - cache.get.flatMap { c => - IO.fromOption(c.get(address))(AclNotFound(address)) - } - - override def fetchAll: IO[Map[AclAddress, Acl]] = cache.get } object AclSimpleCheck { + private def emptyAclSimpleCheck: IO[AclSimpleCheck] = { + Ref.of[IO, Map[AclAddress, Acl]](Map.empty).map { cache => + val aclCheck = AclCheck( + address => cache.get.flatMap { c => IO.fromOption(c.get(address))(AclNotFound(address)) }, + cache.get + ) + new AclSimpleCheck(cache) { + override def authorizeForOr[E <: Throwable]( + path: AclAddress, + permission: Permission, + identities: Set[Identity] + )(onError: => E): IO[Unit] = + aclCheck.authorizeForOr(path, permission, identities)(onError) + + override def authorizeFor(path: AclAddress, permission: Permission, identities: Set[Identity]): IO[Boolean] = + aclCheck.authorizeFor(path, permission, identities) + + override def authorizeForEveryOr[E <: Throwable](path: AclAddress, permissions: Set[Permission])( + onError: => E + )(implicit caller: Caller): IO[Unit] = + aclCheck.authorizeForEveryOr(path, permissions)(onError) + + override def mapFilterOrRaise[A, B]( + values: immutable.Iterable[A], + extractAddressPermission: A => (AclAddress, Permission), + onAuthorized: A => B, + onFailure: AclAddress => IO[Unit] + )(implicit caller: Caller): IO[Set[B]] = + aclCheck.mapFilterOrRaise(values, extractAddressPermission, onAuthorized, onFailure) + + override def mapFilterAtAddressOrRaise[A, B]( + values: immutable.Iterable[A], + address: AclAddress, + extractPermission: A => Permission, + onAuthorized: A => B, + onFailure: AclAddress => IO[Unit] + )(implicit caller: Caller): IO[Set[B]] = + aclCheck.mapFilterAtAddressOrRaise(values, address, extractPermission, onAuthorized, onFailure) + } + } + } + /** * Create an [[AclSimpleCheck]] and initializes it with the provided acls * @param input + * the acls to append to the checker * @return */ def apply(input: (Identity, AclAddress, Set[Permission])*): IO[AclSimpleCheck] = - Ref.of[IO, Map[AclAddress, Acl]](Map.empty).map(new AclSimpleCheck(_)).flatTap { checker => + emptyAclSimpleCheck.flatTap { checker => input.toList .traverse { case (subject, address, permissions) => checker append (address, (subject, permissions)) } } - def unsafe(input: (Identity, AclAddress, Set[Permission])*) = + def unsafe(input: (Identity, AclAddress, Set[Permission])*): AclSimpleCheck = apply(input: _*).unsafeRunSync() } diff --git a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/provisioning/ProjectProvisioningSpec.scala b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/provisioning/ProjectProvisioningSpec.scala index 415f27613c..974e401f51 100644 --- a/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/provisioning/ProjectProvisioningSpec.scala +++ b/delta/sdk/src/test/scala/ch/epfl/bluebrain/nexus/delta/sdk/provisioning/ProjectProvisioningSpec.scala @@ -4,7 +4,7 @@ import cats.effect.IO import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF import ch.epfl.bluebrain.nexus.delta.rdf.syntax.iriStringContextSyntax import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclSimpleCheck -import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.{Acl, AclAddress} +import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.AclAddress import ch.epfl.bluebrain.nexus.delta.sdk.model.BaseUri import ch.epfl.bluebrain.nexus.delta.sdk.organizations.FetchActiveOrganization import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.Organization @@ -62,7 +62,7 @@ class ProjectProvisioningSpec extends CatsEffectSpec with DoobieScalaTestFixture clock ) - private lazy val provisioning = ProjectProvisioning(aclCheck.append(_), projects, provisioningConfig) + private lazy val provisioning = ProjectProvisioning(aclCheck.append, projects, provisioningConfig) "Provisioning projects" should { @@ -70,7 +70,6 @@ class ProjectProvisioningSpec extends CatsEffectSpec with DoobieScalaTestFixture val subject: Subject = Identity.User("user1######", Label.unsafe("realm")) val projectLabel = Label.unsafe("user1") val projectRef = ProjectRef(usersOrg, projectLabel) - val acl = Acl(AclAddress.Project(projectRef), subject -> provisioningConfig.permissions) provisioning(subject).accepted projects.fetchProject(projectRef).accepted shouldEqual Project( projectLabel, @@ -85,7 +84,10 @@ class ProjectProvisioningSpec extends CatsEffectSpec with DoobieScalaTestFixture enforceSchema = provisioningConfig.fields.enforceSchema, markedForDeletion = false ) - aclCheck.fetchOne(projectRef).accepted shouldEqual acl + + provisioningConfig.permissions.foreach { permission => + aclCheck.authorizeFor(AclAddress.Project(projectRef), permission, Set(subject)).accepted shouldEqual true + } } "provision project with even if the ACLs have been set before" in { diff --git a/ship/src/main/resources/default.conf b/ship/src/main/resources/ship-default.conf similarity index 100% rename from ship/src/main/resources/default.conf rename to ship/src/main/resources/ship-default.conf diff --git a/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/ContextWiring.scala b/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/ContextWiring.scala index 5aaea9751f..8501981bc9 100644 --- a/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/ContextWiring.scala +++ b/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/ContextWiring.scala @@ -2,42 +2,46 @@ package ch.epfl.bluebrain.nexus.ship import cats.effect.IO import ch.epfl.bluebrain.nexus.delta.kernel.utils.ClasspathResourceLoader +import ch.epfl.bluebrain.nexus.delta.plugins.blazegraph.model.{contexts => bgContexts} +import ch.epfl.bluebrain.nexus.delta.plugins.compositeviews.model.{contexts => compositeViewContexts} +import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.model.{contexts => esContexts} import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.contexts import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.JsonLdApi import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteContextResolution} -import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContext import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution import ch.epfl.bluebrain.nexus.delta.sdk.resources.FetchResource import ch.epfl.bluebrain.nexus.delta.sourcing.Transactors import ch.epfl.bluebrain.nexus.delta.sourcing.config.EventLogConfig -import ch.epfl.bluebrain.nexus.ship.acls.AclWiring +import ch.epfl.bluebrain.nexus.ship.acls.AclWiring.alwaysAuthorize import ch.epfl.bluebrain.nexus.ship.resolvers.ResolverWiring -import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.model.{contexts => esContexts} -import ch.epfl.bluebrain.nexus.delta.plugins.blazegraph.model.{contexts => bgContexts} - object ContextWiring { implicit private val loader: ClasspathResourceLoader = ClasspathResourceLoader.withContext(getClass) def remoteContextResolution: IO[RemoteContextResolution] = for { + metadataCtx <- ContextValue.fromFile("contexts/metadata.json") pipelineCtx <- ContextValue.fromFile("contexts/pipeline.json") shaclCtx <- ContextValue.fromFile("contexts/shacl.json") schemasMetaCtx <- ContextValue.fromFile("contexts/schemas-metadata.json") elasticsearchCtx <- ContextValue.fromFile("contexts/elasticsearch.json") blazegraphCtx <- ContextValue.fromFile("contexts/sparql.json") + compositeCtx <- ContextValue.fromFile("contexts/composite-views.json") } yield RemoteContextResolution.fixed( // Delta - contexts.pipeline -> pipelineCtx, + contexts.metadata -> metadataCtx, + contexts.pipeline -> pipelineCtx, // Schema - contexts.shacl -> shaclCtx, - contexts.schemasMetadata -> schemasMetaCtx, + contexts.shacl -> shaclCtx, + contexts.schemasMetadata -> schemasMetaCtx, // ElasticSearch - esContexts.elasticsearch -> elasticsearchCtx, + esContexts.elasticsearch -> elasticsearchCtx, // Blazegraph - bgContexts.blazegraph -> blazegraphCtx + bgContexts.blazegraph -> blazegraphCtx, + // Composite views + compositeViewContexts.compositeViews -> compositeCtx ) def resolverContextResolution( @@ -47,12 +51,11 @@ object ContextWiring { clock: EventClock, xas: Transactors )(implicit jsonLdApi: JsonLdApi): IO[ResolverContextResolution] = { - val aclCheck = AclCheck(AclWiring.acls(config, clock, xas)) val resolvers = ResolverWiring.resolvers(fetchContext, config, clock, xas) for { rcr <- remoteContextResolution - } yield ResolverContextResolution(aclCheck, resolvers, rcr, fetchResource) + } yield ResolverContextResolution(alwaysAuthorize, resolvers, rcr, fetchResource) } } 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 f5acee3a10..c38f78a7b4 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 @@ -16,7 +16,7 @@ import ch.epfl.bluebrain.nexus.ship.projects.ProjectProcessor import ch.epfl.bluebrain.nexus.ship.resolvers.ResolverProcessor import ch.epfl.bluebrain.nexus.ship.resources.{ResourceProcessor, ResourceWiring} import ch.epfl.bluebrain.nexus.ship.schemas.{SchemaProcessor, SchemaWiring} -import ch.epfl.bluebrain.nexus.ship.views.{BlazegraphViewProcessor, ElasticSearchViewProcessor} +import ch.epfl.bluebrain.nexus.ship.views.{BlazegraphViewProcessor, CompositeViewProcessor, ElasticSearchViewProcessor} import fs2.Stream import fs2.io.file.{Files, Path} import io.circe.parser.decode @@ -66,6 +66,7 @@ class RunShip { resourceProcessor = ResourceProcessor(resourceLog, fetchContext, eventClock) esViewsProcessor <- ElasticSearchViewProcessor(fetchContext, rcr, eventLogConfig, eventClock, xas) bgViewsProcessor = BlazegraphViewProcessor(fetchContext, rcr, eventLogConfig, eventClock, xas) + compositeViewsProcessor = CompositeViewProcessor(fetchContext, rcr, eventLogConfig, eventClock, xas) report <- EventProcessor .run( events, @@ -74,7 +75,8 @@ class RunShip { schemaProcessor, resourceProcessor, esViewsProcessor, - bgViewsProcessor + bgViewsProcessor, + compositeViewsProcessor ) } yield report } diff --git a/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/acls/AclWiring.scala b/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/acls/AclWiring.scala index 967962ff5d..a36bfcd50c 100644 --- a/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/acls/AclWiring.scala +++ b/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/acls/AclWiring.scala @@ -1,23 +1,44 @@ package ch.epfl.bluebrain.nexus.ship.acls -import cats.effect.{Clock, IO} -import ch.epfl.bluebrain.nexus.delta.sdk.acls.{Acls, AclsImpl} +import cats.effect.IO +import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck +import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.AclAddress +import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission -import ch.epfl.bluebrain.nexus.delta.sourcing.Transactors -import ch.epfl.bluebrain.nexus.delta.sourcing.config.EventLogConfig +import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity + +import scala.collection.immutable object AclWiring { - def acls(config: EventLogConfig, clock: Clock[IO], xas: Transactors): Acls = { - val permissionSet = Set(Permission.unsafe("resources/read")) - AclsImpl( - IO.pure(permissionSet), - AclsImpl.findUnknownRealms(xas), - permissionSet, - config, - xas, - clock - ) + def alwaysAuthorize: AclCheck = new AclCheck { + override def authorizeForOr[E <: Throwable](path: AclAddress, permission: Permission, identities: Set[Identity])( + onError: => E + ): IO[Unit] = IO.unit + + override def authorizeFor(path: AclAddress, permission: Permission, identities: Set[Identity]): IO[Boolean] = + IO.pure(true) + + override def authorizeForEveryOr[E <: Throwable](path: AclAddress, permissions: Set[Permission])(onError: => E)( + implicit caller: Caller + ): IO[Unit] = IO.unit + + override def mapFilterOrRaise[A, B]( + values: immutable.Iterable[A], + extractAddressPermission: A => (AclAddress, Permission), + onAuthorized: A => B, + onFailure: AclAddress => IO[Unit] + )(implicit caller: Caller): IO[Set[B]] = + IO.pure(values.map(onAuthorized).toSet) + + override def mapFilterAtAddressOrRaise[A, B]( + values: immutable.Iterable[A], + address: AclAddress, + extractPermission: A => Permission, + onAuthorized: A => B, + onFailure: AclAddress => IO[Unit] + )(implicit caller: Caller): IO[Set[B]] = + IO.pure(values.map(onAuthorized).toSet) } } diff --git a/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/config/ShipConfig.scala b/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/config/ShipConfig.scala index 5f5aec7502..e83228e609 100644 --- a/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/config/ShipConfig.scala +++ b/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/config/ShipConfig.scala @@ -25,7 +25,7 @@ object ShipConfig { def merge(externalConfigPath: Option[Path]): IO[(ShipConfig, Config)] = for { externalConfig <- Configs.parseFile(externalConfigPath.map(_.toNioPath.toFile)) - defaultConfig <- Configs.parseResource("default.conf") + defaultConfig <- Configs.parseResource("ship-default.conf") result <- Configs.merge[ShipConfig]("ship", externalConfig, defaultConfig) } yield result 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 52f4ba533c..801c26c5f6 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 @@ -2,7 +2,6 @@ package ch.epfl.bluebrain.nexus.ship.resources import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.JsonLdApi import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution -import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck 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 @@ -11,7 +10,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.schemas.FetchSchema import ch.epfl.bluebrain.nexus.delta.sourcing.config.EventLogConfig import ch.epfl.bluebrain.nexus.delta.sourcing.{ScopedEventLog, Transactors} import ch.epfl.bluebrain.nexus.ship.EventClock -import ch.epfl.bluebrain.nexus.ship.acls.AclWiring +import ch.epfl.bluebrain.nexus.ship.acls.AclWiring.alwaysAuthorize import ch.epfl.bluebrain.nexus.ship.resolvers.ResolverWiring object ResourceWiring { @@ -27,10 +26,9 @@ object ResourceWiring { ): (ResourceLog, FetchResource) = { val rcr = RemoteContextResolution.never // TODO: Use correct RemoteContextResolution val detectChange = DetectChange(false) - val aclCheck = AclCheck(AclWiring.acls(config, clock, xas)) val resolvers = ResolverWiring.resolvers(fetchContext, config, clock, xas) val resourceResolution = - ResourceResolution.schemaResource(aclCheck, resolvers, fetchSchema, excludeDeprecated = false) + ResourceResolution.schemaResource(alwaysAuthorize, resolvers, fetchSchema, excludeDeprecated = false) val validate = ValidateResource(resourceResolution)(rcr) val resourceDef = Resources.definition(validate, detectChange, clock) diff --git a/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/schemas/SchemaWiring.scala b/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/schemas/SchemaWiring.scala index ba0f0b2a79..1bb6c4aea6 100644 --- a/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/schemas/SchemaWiring.scala +++ b/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/schemas/SchemaWiring.scala @@ -3,14 +3,13 @@ package ch.epfl.bluebrain.nexus.ship.schemas import cats.effect.IO import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.JsonLdApi import ch.epfl.bluebrain.nexus.delta.rdf.shacl.ShaclShapesGraph -import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContext import ch.epfl.bluebrain.nexus.delta.sdk.resources.FetchResource import ch.epfl.bluebrain.nexus.delta.sdk.schemas.Schemas.SchemaLog import ch.epfl.bluebrain.nexus.delta.sdk.schemas.{FetchSchema, SchemaImports, Schemas, ValidateSchema} import ch.epfl.bluebrain.nexus.delta.sourcing.config.EventLogConfig import ch.epfl.bluebrain.nexus.delta.sourcing.{ScopedEventLog, Transactors} -import ch.epfl.bluebrain.nexus.ship.acls.AclWiring +import ch.epfl.bluebrain.nexus.ship.acls.AclWiring.alwaysAuthorize import ch.epfl.bluebrain.nexus.ship.resolvers.ResolverWiring import ch.epfl.bluebrain.nexus.ship.{ContextWiring, EventClock} @@ -31,9 +30,8 @@ object SchemaWiring { )(implicit jsonLdApi: JsonLdApi ): SchemaImports = { - val aclCheck = AclCheck(AclWiring.acls(config, clock, xas)) val resolvers = ResolverWiring.resolvers(fetchContext, config, clock, xas) - SchemaImports(aclCheck, resolvers, fetchSchema, fetchResource) + SchemaImports(alwaysAuthorize, resolvers, fetchSchema, fetchResource) } private def validateSchema(implicit api: JsonLdApi): IO[ValidateSchema] = diff --git a/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/views/CompositeViewProcessor.scala b/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/views/CompositeViewProcessor.scala new file mode 100644 index 0000000000..b17e2223d9 --- /dev/null +++ b/ship/src/main/scala/ch/epfl/bluebrain/nexus/ship/views/CompositeViewProcessor.scala @@ -0,0 +1,91 @@ +package ch.epfl.bluebrain.nexus.ship.views + +import cats.effect.IO +import ch.epfl.bluebrain.nexus.delta.kernel.Logger +import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF +import ch.epfl.bluebrain.nexus.delta.plugins.compositeviews.model.CompositeViewEvent._ +import ch.epfl.bluebrain.nexus.delta.plugins.compositeviews.model.CompositeViewRejection.{IncorrectRev, ResourceAlreadyExists} +import ch.epfl.bluebrain.nexus.delta.plugins.compositeviews.model.{CompositeViewEvent, CompositeViewValue} +import ch.epfl.bluebrain.nexus.delta.plugins.compositeviews.{CompositeViews, ValidateCompositeView} +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.JsonLdApi +import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller +import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContext +import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution +import ch.epfl.bluebrain.nexus.delta.sourcing.Transactors +import ch.epfl.bluebrain.nexus.delta.sourcing.config.EventLogConfig +import ch.epfl.bluebrain.nexus.delta.sourcing.model.EntityType +import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject +import ch.epfl.bluebrain.nexus.ship.views.CompositeViewProcessor.logger +import ch.epfl.bluebrain.nexus.ship.{EventClock, EventProcessor, ImportStatus} +import io.circe.Decoder + +import java.util.UUID +import scala.concurrent.duration.DurationInt + +class CompositeViewProcessor(views: UUID => IO[CompositeViews], clock: EventClock) + extends EventProcessor[CompositeViewEvent] { + override def resourceType: EntityType = CompositeViews.entityType + + override def decoder: Decoder[CompositeViewEvent] = CompositeViewEvent.serializer.codec + + override def evaluate(event: CompositeViewEvent): IO[ImportStatus] = + for { + _ <- clock.setInstant(event.instant) + result <- evaluateInternal(event) + } yield result + + private def evaluateInternal(event: CompositeViewEvent): IO[ImportStatus] = { + implicit val s: Subject = event.subject + implicit val c: Caller = Caller(s, Set.empty) + val cRev = event.rev - 1 + + event match { + case e: CompositeViewCreated => views(event.uuid).flatMap(_.create(e.project, e.source)) + case e: CompositeViewUpdated => views(event.uuid).flatMap(_.update(e.id, e.project, cRev, e.source)) + case e: CompositeViewDeprecated => views(event.uuid).flatMap(_.deprecate(e.id, e.project, cRev)) + case e: CompositeViewUndeprecated => views(event.uuid).flatMap(_.undeprecate(e.id, e.project, cRev)) + case _: CompositeViewTagAdded => IO.unit // TODO: Can/should we tag? + } + }.redeemWith( + { + case a: ResourceAlreadyExists => logger.warn(a)("The resource already exists").as(ImportStatus.Dropped) + case i: IncorrectRev => logger.warn(i)("An incorrect revision has been provided").as(ImportStatus.Dropped) + case other => IO.raiseError(other) + }, + _ => IO.pure(ImportStatus.Success) + ) +} + +object CompositeViewProcessor { + + private val logger = Logger[CompositeViewProcessor] + + def apply( + fetchContext: FetchContext, + rcr: ResolverContextResolution, + config: EventLogConfig, + clock: EventClock, + xas: Transactors + )(implicit + jsonLdApi: JsonLdApi + ): CompositeViewProcessor = { + val noValidation = new ValidateCompositeView { + override def apply(uuid: UUID, value: CompositeViewValue): IO[Unit] = IO.unit + } + + val views = (uuid: UUID) => + CompositeViews( + fetchContext, + rcr, + noValidation, + 3.seconds, + config, + xas, + clock + )(jsonLdApi, UUIDF.fixed(uuid)) + + new CompositeViewProcessor(views, clock) + + } + +} diff --git a/ship/src/test/resources/default.conf b/ship/src/test/resources/default.conf deleted file mode 100644 index fba48d6ebe..0000000000 --- a/ship/src/test/resources/default.conf +++ /dev/null @@ -1,55 +0,0 @@ -ship { - base-uri = "http://localhost:8080/v1" - - database { - read = ${ship.database.access} - # Access to database for write access - write = ${ship.database.access} - # Access to database for streaming access (indexing / SSEs) - streaming = ${ship.database.access} - - # when true it creates the tables on service boot - tables-autocreate = false - - cache { - # The max number of tokens in the partition cache - max-size = 1000 - # The duration after an entry in the cache expires - expire-after = 10 minutes - } - - access { - # the database host - host = 127.0.0.1 - # the database port - port = 5432 - # the pool size - pool-size = 10 - } - - name = "postgres" - username = "postgres" - password = "postgres" - } - - event-log { - query-config = { - batch-size = 30 - refresh-strategy = 3s - } - max-duration = 14 seconds - } - - organizations { - values { - # organization example - #obp = "The Open Brain Platform Organization" - } - } - - # Service account configuration for internal operations - service-account { - subject: "delta" - realm: "internal" - } -} \ No newline at end of file diff --git a/ship/src/test/scala/ch/epfl/bluebrain/nexus/ship/EndToEndTest.scala b/ship/src/test/scala/ch/epfl/bluebrain/nexus/ship/ShipIntegrationSpec.scala similarity index 88% rename from ship/src/test/scala/ch/epfl/bluebrain/nexus/ship/EndToEndTest.scala rename to ship/src/test/scala/ch/epfl/bluebrain/nexus/ship/ShipIntegrationSpec.scala index b6ee9b2877..f95011dab1 100644 --- a/ship/src/test/scala/ch/epfl/bluebrain/nexus/ship/EndToEndTest.scala +++ b/ship/src/test/scala/ch/epfl/bluebrain/nexus/ship/ShipIntegrationSpec.scala @@ -22,7 +22,7 @@ import java.nio.file.{Files, Paths} import scala.concurrent.duration.DurationInt import scala.jdk.CollectionConverters.IteratorHasAsScala -class EndToEndTest extends BaseIntegrationSpec { +class ShipIntegrationSpec extends BaseIntegrationSpec { override def beforeAll(): Unit = { super.beforeAll() @@ -128,8 +128,31 @@ class EndToEndTest extends BaseIntegrationSpec { thereShouldBeAView(project, bgView, bgViewJson) } - def thereShouldBeAView(project: ProjectRef, schema: Iri, originalJson: Json): Assertion = { - val encodedIri = UrlUtils.encode(schema.toString) + "transfer a search view" in { + val (project, _, _) = thereIsAProject() + val (searchView, searchViewJson) = thereIsASearchView(project) + + whenTheExportIsRunOnProject(project) + theOldProjectIsDeleted(project) + + weRunTheImporter(project) + weFixThePermissions(project) + + thereShouldBeAViewIgnoringUUID(project, searchView, searchViewJson) + } + + def thereIsASearchView(project: ProjectRef): (Iri, Json) = { + val searchView = nxv + "searchView" + val encodedView = UrlUtils.encode(searchView.toString) + val (viewJson, status) = deltaClient + .getJsonAndStatus(s"/views/${project.organization}/${project.project}/$encodedView", writer) + .accepted + status shouldEqual StatusCodes.OK + searchView -> viewJson + } + + def thereShouldBeAView(project: ProjectRef, view: Iri, originalJson: Json): Assertion = { + val encodedIri = UrlUtils.encode(view.toString) deltaClient .get[Json](s"/views/${project.organization}/${project.project}/$encodedIri", writer) { (json, response) => { @@ -140,6 +163,28 @@ class EndToEndTest extends BaseIntegrationSpec { .accepted } + def thereShouldBeAViewIgnoringUUID(project: ProjectRef, view: Iri, originalJson: Json): Assertion = { + val encodedIri = UrlUtils.encode(view.toString) + + import io.circe.optics.JsonPath.root + val ignoreSourceUUID = root.sources.each.at("_uuid").replace(None) + val ignoreProjectionUUID = root.projections.each.at("_uuid").replace(None) + val ignoreUUID = root.at("_uuid").replace(None) + + val filter = ignoreUUID andThen ignoreSourceUUID andThen ignoreProjectionUUID + + root.sources.`null` + + deltaClient + .get[Json](s"/views/${project.organization}/${project.project}/$encodedIri", writer) { (json, response) => + { + response.status shouldEqual StatusCodes.OK + filter(json) shouldEqual filter(originalJson) + } + } + .accepted + } + def thereIsABlazegraphView(project: ProjectRef): (Iri, Json) = { val simpleBgView = json"""{ "@type": "SparqlView", @@ -392,6 +437,7 @@ class EndToEndTest extends BaseIntegrationSpec { } .accepted } + } }