Skip to content

Commit

Permalink
Migrate acls to Cats Effect (#4409)
Browse files Browse the repository at this point in the history
* Migrate acls to Cats Effect

* Scalafmt + fix tests

---------

Co-authored-by: Simon Dumas <simon.dumas@epfl.ch>
  • Loading branch information
imsdu and Simon Dumas authored Oct 23, 2023
1 parent e2af5d4 commit 7f308eb
Show file tree
Hide file tree
Showing 49 changed files with 412 additions and 649 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import akka.http.javadsl.server.Rejections.validationRejection
import akka.http.scaladsl.model.StatusCodes._
import akka.http.scaladsl.model.Uri.Path
import akka.http.scaladsl.model.Uri.Path._
import akka.http.scaladsl.model.{StatusCode, StatusCodes}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.{Directive1, MalformedQueryParamRejection, Route}
import cats.effect.IO
import cats.syntax.all._
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.JsonLdContext.keywords
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution
Expand All @@ -18,9 +20,9 @@ import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.AclAddressFilter.{AnyOrganiz
import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.AclRejection.AclNotFound
import ch.epfl.bluebrain.nexus.delta.sdk.acls.model._
import ch.epfl.bluebrain.nexus.delta.sdk.acls.{AclCheck, Acls}
import ch.epfl.bluebrain.nexus.delta.sdk.ce.DeltaDirectives._
import ch.epfl.bluebrain.nexus.delta.sdk.circe.CirceUnmarshalling
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.implicits._
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.RdfRejectionHandler.{malformedQueryParamEncoder, malformedQueryParamResponseFields}
Expand All @@ -35,15 +37,11 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Identity, Label, ProjectRef
import io.circe._
import io.circe.generic.extras.Configuration
import io.circe.generic.extras.semiauto.deriveConfiguredDecoder
import kamon.instrumentation.akka.http.TracingDirectives.operationName
import monix.bio.IO
import monix.execution.Scheduler

import scala.annotation.nowarn

class AclsRoutes(identities: Identities, acls: Acls, aclCheck: AclCheck)(implicit
baseUri: BaseUri,
s: Scheduler,
cr: RemoteContextResolution,
ordering: JsonKeyOrdering
) extends AuthDirectives(identities, aclCheck)
Expand All @@ -55,8 +53,6 @@ class AclsRoutes(identities: Identities, acls: Acls, aclCheck: AclCheck)(implici
private val simultaneousRevAndAncestorsRejection =
MalformedQueryParamRejection("rev", "rev and ancestors query parameters cannot be present simultaneously.")

import baseUri.prefixSegment

implicit private val aclsSearchJsonLdEncoder: JsonLdEncoder[SearchResults[AclResource]] =
searchResultsJsonLdEncoder(Acl.context)

Expand Down Expand Up @@ -92,117 +88,113 @@ class AclsRoutes(identities: Identities, acls: Acls, aclCheck: AclCheck)(implici
}
}

private def emitMetadata(statusCode: StatusCode, io: IO[AclResource]): Route =
emit(statusCode, io.mapValue(_.metadata).attemptNarrow[AclRejection])

private def emitMetadata(io: IO[AclResource]): Route = emitMetadata(StatusCodes.OK, io)

private def emitWithoutAncestors(io: IO[AclResource]): Route = emit {
io.map(Option(_))
.recover { case AclNotFound(_) =>
None
}
.map(searchResults(_))
.attemptNarrow[AclRejection]
}

private def emitWithAncestors(io: IO[AclCollection]) =
emit(io.map { collection => searchResults(collection.value.values) })

private def searchResults(iter: Iterable[AclResource]): SearchResults[AclResource] = {
val vector = iter.toVector
SearchResults(vector.length.toLong, vector)
}

def routes: Route =
baseUriPrefix(baseUri.prefix) {
pathPrefix("acls") {
extractCaller { implicit caller =>
concat(
extractAclAddress { address =>
parameter("rev" ? 0) { rev =>
operationName(s"$prefixSegment/acls${address.string}") {
concat(
// Replace ACLs
(put & entity(as[ReplaceAcl])) { case ReplaceAcl(AclValues(values)) =>
authorizeFor(address, aclsPermissions.write).apply {
val status = if (rev == 0) Created else OK
emit(status, acls.replace(Acl(address, values: _*), rev).mapValue(_.metadata))
}
},
// Append or subtract ACLs
(patch & entity(as[PatchAcl]) & authorizeFor(address, aclsPermissions.write)) {
case Append(AclValues(values)) =>
emit(acls.append(Acl(address, values: _*), rev).mapValue(_.metadata))
case Subtract(AclValues(values)) =>
emit(acls.subtract(Acl(address, values: _*), rev).mapValue(_.metadata))
},
// Delete ACLs
delete {
authorizeFor(address, aclsPermissions.write).apply {
emit(acls.delete(address, rev).mapValue(_.metadata))
concat(
// Replace ACLs
(put & entity(as[ReplaceAcl])) { case ReplaceAcl(AclValues(values)) =>
authorizeFor(address, aclsPermissions.write).apply {
val status = if (rev == 0) Created else OK
emitMetadata(status, acls.replace(Acl(address, values: _*), rev))
}
},
// Append or subtract ACLs
(patch & entity(as[PatchAcl]) & authorizeFor(address, aclsPermissions.write)) {
case Append(AclValues(values)) =>
emitMetadata(acls.append(Acl(address, values: _*), rev))
case Subtract(AclValues(values)) =>
emitMetadata(acls.subtract(Acl(address, values: _*), rev))
},
// Delete ACLs
delete {
authorizeFor(address, aclsPermissions.write).apply {
emitMetadata(acls.delete(address, rev))
}
},
// Fetch ACLs
(get & parameter("self" ? true)) {
case true =>
(parameter("rev".as[Int].?) & parameter("ancestors" ? false)) {
case (Some(_), true) => emit(simultaneousRevAndAncestorsRejection)
case (Some(rev), false) =>
// Fetch self ACLs without ancestors at specific revision
emitWithoutAncestors(acls.fetchSelfAt(address, rev))
case (None, true) =>
// Fetch self ACLs with ancestors
emitWithAncestors(acls.fetchSelfWithAncestors(address))
case (None, false) =>
// Fetch self ACLs without ancestors
emitWithoutAncestors(acls.fetchSelf(address))
}
},
// Fetch ACLs
(get & parameter("self" ? true)) {
case true =>
case false =>
authorizeFor(address, aclsPermissions.read).apply {
(parameter("rev".as[Int].?) & parameter("ancestors" ? false)) {
case (Some(_), true) => emit(simultaneousRevAndAncestorsRejection)
case (Some(_), true) => reject(simultaneousRevAndAncestorsRejection)
case (Some(rev), false) =>
// Fetch self ACLs without ancestors at specific revision
emit(notFoundToNone(acls.fetchSelfAt(address, rev)).map(searchResults(_)))
// Fetch all ACLs without ancestors at specific revision
emitWithoutAncestors(acls.fetchAt(address, rev))
case (None, true) =>
// Fetch self ACLs with ancestors
emit(acls.fetchSelfWithAncestors(address).map(col => searchResults(col.value.values)))
// Fetch all ACLs with ancestors
emitWithAncestors(acls.fetchWithAncestors(address))
case (None, false) =>
// Fetch self ACLs without ancestors
emit(notFoundToNone(acls.fetchSelf(address)).map(searchResults(_)))
// Fetch all ACLs without ancestors
emitWithoutAncestors(acls.fetch(address))
}
case false =>
authorizeFor(address, aclsPermissions.read).apply {
(parameter("rev".as[Int].?) & parameter("ancestors" ? false)) {
case (Some(_), true) => reject(simultaneousRevAndAncestorsRejection)
case (Some(rev), false) =>
// Fetch all ACLs without ancestors at specific revision
emit(notFoundToNone(acls.fetchAt(address, rev)).map(searchResults(_)))
case (None, true) =>
// Fetch all ACLs with ancestors
emit(acls.fetchWithAncestors(address).map(col => searchResults(col.value.values)))
case (None, false) =>
// Fetch all ACLs without ancestors
emit(notFoundToNone(acls.fetch(address)).map(searchResults(_)))
}
}
}
)
}
}
}
)
}
},
// Filter ACLs
(get & extractAclAddressFilter) { addressFilter =>
operationName(s"$prefixSegment/acls${addressFilter.string}") {
parameter("self" ? true) {
case true =>
// Filter self ACLs with or without ancestors
emit(
acls
.listSelf(addressFilter)
.map { aclCol =>
val nonEmpty = aclCol.removeEmpty()
SearchResults(nonEmpty.value.size.toLong, nonEmpty.value.values.toSeq)
}
.widen[SearchResults[AclResource]]
)
case false =>
// Filter all ACLs with or without ancestors
emit(
acls
.list(addressFilter)
.map { aclCol =>
val accessibleAcls = aclCol.filterByPermission(caller.identities, aclsPermissions.read)
val callerAcls = aclCol.filter(caller.identities)
val acls = accessibleAcls ++ callerAcls
SearchResults(acls.value.size.toLong, acls.value.values.toSeq)
}
.widen[SearchResults[AclResource]]
)
}
parameter("self" ? true) {
case true =>
// Filter self ACLs with or without ancestors
emitWithAncestors(acls.listSelf(addressFilter).map(_.removeEmpty()))
case false =>
// Filter all ACLs with or without ancestors
emitWithAncestors(
acls
.list(addressFilter)
.map { aclCol =>
val accessibleAcls = aclCol.filterByPermission(caller.identities, aclsPermissions.read)
val callerAcls = aclCol.filter(caller.identities)
accessibleAcls ++ callerAcls
}
)
}
}
)
}
}
}

private def notFoundToNone(result: IO[AclRejection, AclResource]): IO[AclRejection, Option[AclResource]] =
result.attempt.flatMap {
case Right(resource) => IO.pure(Some(resource))
case Left(AclNotFound(_)) => IO.none
case Left(rejection) => IO.raiseError(rejection)
}

private def searchResults(iter: Iterable[AclResource]): SearchResults[AclResource] = {
val vector = iter.toVector
SearchResults(vector.length.toLong, vector)
}
}

object AclsRoutes {
Expand Down Expand Up @@ -253,7 +245,6 @@ object AclsRoutes {
*/
def apply(identities: Identities, acls: Acls, aclCheck: AclCheck)(implicit
baseUri: BaseUri,
s: Scheduler,
cr: RemoteContextResolution,
ordering: JsonKeyOrdering
): AclsRoutes = new AclsRoutes(identities, acls, aclCheck)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering
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.AuthDirectives
import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._
import ch.epfl.bluebrain.nexus.delta.sdk.ce.DeltaDirectives._
import ch.epfl.bluebrain.nexus.delta.sdk.directives.UriDirectives.baseUriPrefix
import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.RdfMarshalling
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceRepresentation}
import ch.epfl.bluebrain.nexus.delta.sdk.multifetch.MultiFetch
import ch.epfl.bluebrain.nexus.delta.sdk.multifetch.model.MultiFetchRequest
import io.circe.Printer
import monix.execution.Scheduler

/**
* Route allowing to fetch multiple resources in a single request
Expand All @@ -27,8 +26,7 @@ class MultiFetchRoutes(
)(implicit
baseUri: BaseUri,
cr: RemoteContextResolution,
ordering: JsonKeyOrdering,
s: Scheduler
ordering: JsonKeyOrdering
) extends AuthDirectives(identities, aclCheck)
with CirceUnmarshalling
with RdfMarshalling {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.{Directive1, Route}
import cats.effect.IO
import cats.implicits._
import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder
import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering
Expand All @@ -17,10 +18,10 @@ import ch.epfl.bluebrain.nexus.delta.sdk.directives.{AuthDirectives, DeltaScheme
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._
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceF}
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchParams.OrganizationSearchParams
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults._
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.{PaginationConfig, SearchResults}
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceF}
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationRejection._
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.{Organization, OrganizationRejection}
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.{OrganizationDeleter, Organizations}
Expand Down Expand Up @@ -62,16 +63,17 @@ final class OrganizationsRoutes(
import schemeDirectives._

private def orgsSearchParams(implicit caller: Caller): Directive1[OrganizationSearchParams] =
(searchParams & parameter("label".?)).tmap { case (deprecated, rev, createdBy, updatedBy, label) =>
val fetchAllCached = aclCheck.fetchAll.memoizeOnSuccess
OrganizationSearchParams(
deprecated,
rev,
createdBy,
updatedBy,
label,
org => aclCheck.authorizeFor(org.label, orgs.read, fetchAllCached)
)
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).toUIO
)
}
}

private def emitMetadata(value: IO[OrganizationResource]) = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder
import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering
import ch.epfl.bluebrain.nexus.delta.sdk._
import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._
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._
Expand Down Expand Up @@ -64,19 +65,21 @@ final class ProjectsRoutes(

implicit val paginationConfig: PaginationConfig = config.pagination

private def projectsSearchParams(implicit caller: Caller): Directive1[ProjectSearchParams] =
(searchParams & parameter("label".?)).tmap { case (deprecated, rev, createdBy, updatedBy, label) =>
val fetchAllCached = aclCheck.fetchAll.memoizeOnSuccess
ProjectSearchParams(
None,
deprecated,
rev,
createdBy,
updatedBy,
label,
proj => aclCheck.authorizeFor(proj.ref, projectsPermissions.read, fetchAllCached)
)
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, projectsPermissions.read, allAcls).toUIO
)
}
}
}

private def provisionProject(implicit caller: Caller): Directive0 = onSuccess(
projectProvisioning(caller.subject).runToFuture
Expand Down
Loading

0 comments on commit 7f308eb

Please sign in to comment.