From 9d1391da3fe122d4125d4b8ebc57c985ee8c1511 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Wed, 27 Mar 2024 16:32:31 +0100 Subject: [PATCH 1/5] Make AclCheck trait independent of Acl --- .../delta/routes/OrganizationsRoutes.scala | 23 +- .../nexus/delta/routes/ProjectsRoutes.scala | 22 +- .../nexus/delta/sdk/acls/AclCheck.scala | 261 ++++++++++-------- .../delta/sdk/multifetch/MultiFetch.scala | 5 +- 4 files changed, 170 insertions(+), 141 deletions(-) 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/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..eb72998599 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 @@ -47,47 +27,17 @@ trait AclCheck { */ def authorizeForOr[E <: Throwable](path: AclAddress, permission: Permission)(onError: => E)(implicit caller: Caller - ): IO[Unit] = - authorizeForOr(path, permission, caller.identities)(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) - } + ): IO[Unit] /** * 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''. */ - def authorizeFor(path: AclAddress, permission: Permission)(implicit caller: Caller): IO[Boolean] = - authorizeFor(path, permission, caller.identities) + def authorizeFor(path: AclAddress, permission: Permission)(implicit caller: Caller): IO[Boolean] /** * Checks whether a given [[Caller]] has all the passed ''permissions'' on the passed ''path'', raising the error @@ -95,26 +45,7 @@ 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. @@ -135,17 +66,7 @@ trait AclCheck { 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. @@ -161,8 +82,7 @@ trait AclCheck { values: Iterable[A], extractAddressPermission: A => (AclAddress, Permission), onAuthorized: A => B - )(implicit caller: Caller): IO[Set[B]] = - mapFilterOrRaise(values, extractAddressPermission, onAuthorized, _ => IO.unit) + )(implicit caller: Caller): IO[Set[B]] /** * Map authorized values for the provided caller. @@ -170,7 +90,7 @@ trait AclCheck { * 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 address * the address to check for * @param extractPermission @@ -186,30 +106,13 @@ trait AclCheck { 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. * * @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 @@ -222,19 +125,151 @@ trait AclCheck { address: AclAddress, extractPermission: A => Permission, onAuthorized: A => B - )(implicit caller: Caller): IO[Set[B]] = - mapFilterAtAddressOrRaise(values, address, extractPermission, onAuthorized, _ => IO.unit) + )(implicit caller: Caller): IO[Set[B]] + } object AclCheck { def apply(acls: Acls): AclCheck = new AclCheck { - override def fetchOne: AclAddress => IO[Acl] = acls.fetch(_).map(_.value) + def fetchOne: AclAddress => IO[Acl] = acls.fetch(_).map(_.value) - override def fetchAll: IO[Map[AclAddress, Acl]] = + def fetchAll: IO[Map[AclAddress, Acl]] = acls .list(AnyOrganizationAnyProject(true)) .map(_.value.map { case (address, resource) => address -> resource.value }) + + 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 authorizeForOr[E <: Throwable](path: AclAddress, permission: Permission)(onError: => E)(implicit + caller: Caller + ): IO[Unit] = authorizeForOr(path, permission, caller.identities)(onError) + + override def authorizeFor(path: AclAddress, permission: Permission, identities: Set[Identity]): IO[Boolean] = + authorizeFor(path, permission, identities, fetchOne) + + override def authorizeFor(path: AclAddress, permission: Permission)(implicit caller: Caller): IO[Boolean] = + authorizeFor(path, permission, caller.identities) + + 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[E, 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) + } + } + } + + override def mapFilter[A, B]( + values: Iterable[A], + extractAddressPermission: A => (AclAddress, Permission), + onAuthorized: A => B + )(implicit caller: Caller): IO[Set[B]] = + mapFilterOrRaise(values, extractAddressPermission, onAuthorized, _ => IO.unit) + + override def mapFilterAtAddressOrRaise[E, 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) + } + } + } + + override def mapFilterAtAddress[A, B]( + values: Iterable[A], + address: AclAddress, + extractPermission: A => Permission, + onAuthorized: A => B + )(implicit caller: Caller): IO[Set[B]] = + mapFilterAtAddressOrRaise(values, address, extractPermission, onAuthorized, _ => IO.unit) } } 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) } - } } } From e6afb7c64d42bf8d1a994bb145ed4ab90024a3e0 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Wed, 27 Mar 2024 17:24:04 +0100 Subject: [PATCH 2/5] Refactor AclSimpleCheck --- .../nexus/delta/sdk/acls/AclCheck.scala | 21 +++-- .../nexus/delta/sdk/acls/AclCheckSuite.scala | 16 ++-- .../nexus/delta/sdk/acls/AclSimpleCheck.scala | 79 ++++++++++++++++--- .../ProjectProvisioningSpec.scala | 10 ++- 4 files changed, 94 insertions(+), 32 deletions(-) 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 eb72998599..38711f332d 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 @@ -53,7 +53,7 @@ trait AclCheck { * 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 @@ -69,7 +69,7 @@ trait AclCheck { )(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 @@ -109,7 +109,7 @@ trait AclCheck { )(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 address permission to check @@ -131,13 +131,18 @@ trait AclCheck { object AclCheck { - def apply(acls: Acls): AclCheck = new AclCheck { - def fetchOne: AclAddress => IO[Acl] = acls.fetch(_).map(_.value) - - 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, @@ -146,7 +151,7 @@ object AclCheck { f: AclAddress => IO[Acl] )( onError: => E - ): IO[Unit] = + ): IO[Unit] = authorizeFor(path, permission, identities, f) .flatMap { result => IO.raiseWhen(!result)(onError) } 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..8066ce891e 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 => 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..31f5f310a6 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 @@ -8,11 +8,14 @@ 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 ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller + +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,24 +36,82 @@ 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 authorizeForOr[E <: Throwable](path: AclAddress, permission: Permission)(onError: => E)(implicit + caller: Caller + ): IO[Unit] = + aclCheck.authorizeForOr(path, permission)(onError) + + override def authorizeFor(path: AclAddress, permission: Permission, identities: Set[Identity]): IO[Boolean] = + aclCheck.authorizeFor(path, permission, identities) + + override def authorizeFor(path: AclAddress, permission: Permission)(implicit caller: Caller): IO[Boolean] = + aclCheck.authorizeFor(path, permission) + + 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[E, 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 mapFilter[A, B]( + values: immutable.Iterable[A], + extractAddressPermission: A => (AclAddress, Permission), + onAuthorized: A => B + )(implicit caller: Caller): IO[Set[B]] = + aclCheck.mapFilter(values, extractAddressPermission, onAuthorized) + + override def mapFilterAtAddressOrRaise[E, 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) + + override def mapFilterAtAddress[A, B]( + values: immutable.Iterable[A], + address: AclAddress, + extractPermission: A => Permission, + onAuthorized: A => B + )(implicit caller: Caller): IO[Set[B]] = + aclCheck.mapFilterAtAddress(values, address, extractPermission, onAuthorized) + } + } + } + /** * Create an [[AclSimpleCheck]] and initializes it with the provided acls * @param input * @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)) 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 { From c93beb90f64ee7484f84ac7ce347e44d7a156e0b Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Wed, 27 Mar 2024 17:37:27 +0100 Subject: [PATCH 3/5] Cleaner AclCheck trait --- .../nexus/delta/sdk/acls/AclCheck.scala | 46 +++++-------------- .../nexus/delta/sdk/acls/AclSimpleCheck.scala | 32 ++----------- 2 files changed, 16 insertions(+), 62 deletions(-) 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 38711f332d..35563b828e 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 @@ -27,7 +27,8 @@ trait AclCheck { */ def authorizeForOr[E <: Throwable](path: AclAddress, permission: Permission)(onError: => E)(implicit caller: Caller - ): IO[Unit] + ): IO[Unit] = + authorizeForOr(path, permission, caller.identities)(onError) /** * Checks whether the provided entities have the passed ''permission'' on the passed ''path''. @@ -37,7 +38,8 @@ trait AclCheck { /** * Checks whether a given [[Caller]] has the passed ''permission'' on the passed ''path''. */ - def authorizeFor(path: AclAddress, permission: Permission)(implicit caller: Caller): IO[Boolean] + def authorizeFor(path: AclAddress, permission: Permission)(implicit caller: Caller): IO[Boolean] = + authorizeFor(path, permission, caller.identities) /** * Checks whether a given [[Caller]] has all the passed ''permissions'' on the passed ''path'', raising the error @@ -82,12 +84,11 @@ trait AclCheck { values: Iterable[A], extractAddressPermission: A => (AclAddress, Permission), onAuthorized: A => B - )(implicit caller: Caller): IO[Set[B]] + )(implicit caller: Caller): IO[Set[B]] = + 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 address permission to check @@ -97,10 +98,8 @@ trait AclCheck { * 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, @@ -125,7 +124,8 @@ trait AclCheck { address: AclAddress, extractPermission: A => Permission, onAuthorized: A => B - )(implicit caller: Caller): IO[Set[B]] + )(implicit caller: Caller): IO[Set[B]] = + mapFilterAtAddressOrRaise(values, address, extractPermission, onAuthorized, _ => IO.unit) } @@ -186,16 +186,9 @@ object AclCheck { onError: => E ): IO[Unit] = authorizeForOrFail(path, permission, identities, fetchOne)(onError) - override def authorizeForOr[E <: Throwable](path: AclAddress, permission: Permission)(onError: => E)(implicit - caller: Caller - ): IO[Unit] = authorizeForOr(path, permission, caller.identities)(onError) - override def authorizeFor(path: AclAddress, permission: Permission, identities: Set[Identity]): IO[Boolean] = authorizeFor(path, permission, identities, fetchOne) - override def authorizeFor(path: AclAddress, permission: Permission)(implicit caller: Caller): IO[Boolean] = - authorizeFor(path, permission, caller.identities) - override def authorizeForEveryOr[E <: Throwable](path: AclAddress, permissions: Set[Permission])(onError: => E)( implicit caller: Caller ): IO[Unit] = @@ -219,7 +212,7 @@ object AclCheck { } .flatMap { case (result, _) => IO.raiseWhen(!result)(onError) } - override def mapFilterOrRaise[E, A, B]( + override def mapFilterOrRaise[A, B]( values: Iterable[A], extractAddressPermission: A => (AclAddress, Permission), onAuthorized: A => B, @@ -236,14 +229,7 @@ object AclCheck { } } - override def mapFilter[A, B]( - values: Iterable[A], - extractAddressPermission: A => (AclAddress, Permission), - onAuthorized: A => B - )(implicit caller: Caller): IO[Set[B]] = - mapFilterOrRaise(values, extractAddressPermission, onAuthorized, _ => IO.unit) - - override def mapFilterAtAddressOrRaise[E, A, B]( + def mapFilterAtAddressOrRaise[E, A, B]( values: Iterable[A], address: AclAddress, extractPermission: A => Permission, @@ -267,14 +253,6 @@ object AclCheck { } } } - - override def mapFilterAtAddress[A, B]( - values: Iterable[A], - address: AclAddress, - extractPermission: A => Permission, - onAuthorized: A => B - )(implicit caller: Caller): IO[Set[B]] = - mapFilterAtAddressOrRaise(values, address, extractPermission, onAuthorized, _ => IO.unit) } } 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 31f5f310a6..5f3b3092f7 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,14 +1,13 @@ 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 ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller import scala.collection.immutable @@ -54,17 +53,9 @@ object AclSimpleCheck { )(onError: => E): IO[Unit] = aclCheck.authorizeForOr(path, permission, identities)(onError) - override def authorizeForOr[E <: Throwable](path: AclAddress, permission: Permission)(onError: => E)(implicit - caller: Caller - ): IO[Unit] = - aclCheck.authorizeForOr(path, permission)(onError) - override def authorizeFor(path: AclAddress, permission: Permission, identities: Set[Identity]): IO[Boolean] = aclCheck.authorizeFor(path, permission, identities) - override def authorizeFor(path: AclAddress, permission: Permission)(implicit caller: Caller): IO[Boolean] = - aclCheck.authorizeFor(path, permission) - override def authorizeForEveryOr[E <: Throwable](path: AclAddress, permissions: Set[Permission])( onError: => E )(implicit caller: Caller): IO[Unit] = @@ -78,14 +69,7 @@ object AclSimpleCheck { )(implicit caller: Caller): IO[Set[B]] = aclCheck.mapFilterOrRaise(values, extractAddressPermission, onAuthorized, onFailure) - override def mapFilter[A, B]( - values: immutable.Iterable[A], - extractAddressPermission: A => (AclAddress, Permission), - onAuthorized: A => B - )(implicit caller: Caller): IO[Set[B]] = - aclCheck.mapFilter(values, extractAddressPermission, onAuthorized) - - override def mapFilterAtAddressOrRaise[E, A, B]( + override def mapFilterAtAddressOrRaise[A, B]( values: immutable.Iterable[A], address: AclAddress, extractPermission: A => Permission, @@ -93,14 +77,6 @@ object AclSimpleCheck { onFailure: AclAddress => IO[Unit] )(implicit caller: Caller): IO[Set[B]] = aclCheck.mapFilterAtAddressOrRaise(values, address, extractPermission, onAuthorized, onFailure) - - override def mapFilterAtAddress[A, B]( - values: immutable.Iterable[A], - address: AclAddress, - extractPermission: A => Permission, - onAuthorized: A => B - )(implicit caller: Caller): IO[Set[B]] = - aclCheck.mapFilterAtAddress(values, address, extractPermission, onAuthorized) } } } From 214c836ddadcfdb24c2eaf401dda9d2815d8f876 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Wed, 27 Mar 2024 17:46:51 +0100 Subject: [PATCH 4/5] Clean function signatures --- .../ch/epfl/bluebrain/nexus/delta/sdk/acls/AclCheck.scala | 6 ++---- .../bluebrain/nexus/delta/sdk/acls/AclCheckSuite.scala | 8 ++++---- .../bluebrain/nexus/delta/sdk/acls/AclSimpleCheck.scala | 5 +++-- 3 files changed, 9 insertions(+), 10 deletions(-) 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 35563b828e..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 @@ -52,8 +52,6 @@ trait AclCheck { /** * Map authorized values for the provided caller. * - * Will raise an error [[E]] at the first unauthorized attempt - * * @param values * the list of couples address permission to check * @param extractAddressPermission @@ -63,7 +61,7 @@ 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, @@ -229,7 +227,7 @@ object AclCheck { } } - def mapFilterAtAddressOrRaise[E, A, B]( + def mapFilterAtAddressOrRaise[A, B]( values: Iterable[A], address: AclAddress, extractPermission: A => Permission, 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 8066ce891e..cab21701d3 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 @@ -98,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, @@ -119,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, @@ -147,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, @@ -170,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 5f3b3092f7..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 @@ -61,7 +61,7 @@ object AclSimpleCheck { )(implicit caller: Caller): IO[Unit] = aclCheck.authorizeForEveryOr(path, permissions)(onError) - override def mapFilterOrRaise[E, A, B]( + override def mapFilterOrRaise[A, B]( values: immutable.Iterable[A], extractAddressPermission: A => (AclAddress, Permission), onAuthorized: A => B, @@ -84,6 +84,7 @@ object AclSimpleCheck { /** * 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] = @@ -94,7 +95,7 @@ object AclSimpleCheck { } } - def unsafe(input: (Identity, AclAddress, Set[Permission])*) = + def unsafe(input: (Identity, AclAddress, Set[Permission])*): AclSimpleCheck = apply(input: _*).unsafeRunSync() } From c65588c0f246bc0450a06137abaace90e2ff4545 Mon Sep 17 00:00:00 2001 From: Oliver <20188437+olivergrabinski@users.noreply.github.com> Date: Thu, 28 Mar 2024 08:40:16 +0100 Subject: [PATCH 5/5] Fix test --- .../bluebrain/nexus/delta/sdk/acls/AclCheckSuite.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 cab21701d3..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 @@ -33,10 +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.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) + 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 =>