diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/UserPermissionsRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/UserPermissionsRoutes.scala index 7854f8ad65..faea7230b7 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/UserPermissionsRoutes.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/UserPermissionsRoutes.scala @@ -3,12 +3,15 @@ package ch.epfl.bluebrain.nexus.delta.routes import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route +import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration.MigrateEffectSyntax 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.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.model.BaseUri +import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, IdSegmentRef} +import ch.epfl.bluebrain.nexus.delta.sdk.permissions.StoragePermissionProvider import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission /** @@ -19,10 +22,11 @@ import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission * @param aclCheck * verify the acls for users */ -final class UserPermissionsRoutes(identities: Identities, aclCheck: AclCheck)(implicit - baseUri: BaseUri +final class UserPermissionsRoutes(identities: Identities, aclCheck: AclCheck, storages: StoragePermissionProvider)( + implicit baseUri: BaseUri ) extends AuthDirectives(identities, aclCheck) - with CirceUnmarshalling { + with CirceUnmarshalling + with MigrateEffectSyntax { def routes: Route = baseUriPrefix(baseUri.prefix) { @@ -31,11 +35,21 @@ final class UserPermissionsRoutes(identities: Identities, aclCheck: AclCheck)(im projectRef { project => extractCaller { implicit caller => head { - parameter("permission".as[Permission]) { permission => - authorizeFor(project, permission)(caller) { - complete(StatusCodes.NoContent) + concat( + parameter("permission".as[Permission]) { permission => + authorizeFor(project, permission)(caller) { + complete(StatusCodes.NoContent) + } + }, + parameters("storage", "type") { (storageId, `type`) => + authorizeForAsync( + AclAddress.fromProject(project), + storages.permissionFor(IdSegmentRef(storageId), project, `type` == "read") + )(caller) { + complete(StatusCodes.NoContent) + } } - } + ) } } } @@ -45,8 +59,8 @@ final class UserPermissionsRoutes(identities: Identities, aclCheck: AclCheck)(im } object UserPermissionsRoutes { - def apply(identities: Identities, aclCheck: AclCheck)(implicit + def apply(identities: Identities, aclCheck: AclCheck, storagePermissionProvider: StoragePermissionProvider)(implicit baseUri: BaseUri ): Route = - new UserPermissionsRoutes(identities, aclCheck: AclCheck).routes + new UserPermissionsRoutes(identities, aclCheck: AclCheck, storagePermissionProvider).routes } diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/AclsModule.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/AclsModule.scala index 7502e8eff5..cccfca2051 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/AclsModule.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/AclsModule.scala @@ -14,7 +14,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.acls.{AclCheck, Acls, AclsImpl} import ch.epfl.bluebrain.nexus.delta.sdk.deletion.ProjectDeletionTask import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, MetadataContextValue} -import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions +import ch.epfl.bluebrain.nexus.delta.sdk.permissions.{Permissions, StoragePermissionProvider} import ch.epfl.bluebrain.nexus.delta.sdk.sse.SseEncoder import ch.epfl.bluebrain.nexus.delta.sourcing.Transactors import izumi.distage.model.definition.{Id, ModuleDef} @@ -72,8 +72,14 @@ object AclsModule extends ModuleDef { } yield RemoteContextResolution.fixed(contexts.acls -> aclsCtx, contexts.aclsMetadata -> aclsMetaCtx) ) - make[UserPermissionsRoutes].from { (identities: Identities, aclCheck: AclCheck, baseUri: BaseUri) => - new UserPermissionsRoutes(identities, aclCheck)(baseUri) + make[UserPermissionsRoutes].from { + ( + identities: Identities, + aclCheck: AclCheck, + baseUri: BaseUri, + storagePermissionProvider: StoragePermissionProvider + ) => + new UserPermissionsRoutes(identities, aclCheck, storagePermissionProvider)(baseUri) } many[PriorityRoute].add { (alcs: AclsRoutes, userPermissions: UserPermissionsRoutes) => diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/StoragePluginModule.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/StoragePluginModule.scala index 03f4609bfd..ab6c097980 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/StoragePluginModule.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/StoragePluginModule.scala @@ -33,14 +33,15 @@ import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.ServiceAccount import ch.epfl.bluebrain.nexus.delta.sdk.model._ import ch.epfl.bluebrain.nexus.delta.sdk.model.metrics.ScopedEventMetricEncoder -import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions +import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission +import ch.epfl.bluebrain.nexus.delta.sdk.permissions.{Permissions, StoragePermissionProvider} import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContext import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContext.ContextRejection import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ApiMappings import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution import ch.epfl.bluebrain.nexus.delta.sdk.sse.SseEncoder import ch.epfl.bluebrain.nexus.delta.sourcing.Transactors -import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label +import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef} import ch.epfl.bluebrain.nexus.delta.sourcing.stream.Supervisor import com.typesafe.config.Config import izumi.distage.model.definition.{Id, ModuleDef} @@ -94,6 +95,22 @@ class StoragePluginModule(priority: Int) extends ModuleDef { ) } + make[StoragePermissionProvider].from { (storages: Storages) => + new StoragePermissionProvider { + override def permissionFor(id: IdSegmentRef, project: ProjectRef, read: Boolean): UIO[Permission] = + storages + .fetch(id, project) + .map(storage => storage.value.storageValue) + .map(storage => + read match { + case true => storage.readPermission + case false => storage.writePermission + } + ) + .hideErrorsWith(_ => new RuntimeException("bob")) + } + } + make[StoragesStatistics].from { ( client: ElasticSearchClient, diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/AuthDirectives.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/AuthDirectives.scala index 1354cc85f1..0a537b090c 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/AuthDirectives.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/AuthDirectives.scala @@ -68,6 +68,11 @@ abstract class AuthDirectives(identities: Identities, aclCheck: AclCheck) { def authorizeFor(path: AclAddress, permission: Permission)(implicit caller: Caller): Directive0 = authorizeAsync(toCatsIO(aclCheck.authorizeFor(path, permission)).unsafeToFuture()) or failWith(AuthorizationFailed) + def authorizeForAsync(path: AclAddress, fetchPermission: IO[Permission])(implicit caller: Caller): Directive0 = { + val check = fetchPermission.flatMap(permission => toCatsIO(aclCheck.authorizeFor(path, permission))) + authorizeAsync(check.unsafeToFuture()) or failWith(AuthorizationFailed) + } + /** * Check whether [[Caller]] is the configured service account. */ diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/permissions/StoragePermissionProvider.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/permissions/StoragePermissionProvider.scala new file mode 100644 index 0000000000..211055fd85 --- /dev/null +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/permissions/StoragePermissionProvider.scala @@ -0,0 +1,12 @@ +package ch.epfl.bluebrain.nexus.delta.sdk.permissions + +import ch.epfl.bluebrain.nexus.delta.sdk.model.IdSegmentRef +import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission +import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef +import monix.bio.UIO + +trait StoragePermissionProvider { + + def permissionFor(id: IdSegmentRef, project: ProjectRef, read: Boolean): UIO[Permission] + +} diff --git a/tests/src/test/resources/kg/storages/disk-perms-parameterised.json b/tests/src/test/resources/kg/storages/disk-perms-parameterised.json new file mode 100644 index 0000000000..e54e31da89 --- /dev/null +++ b/tests/src/test/resources/kg/storages/disk-perms-parameterised.json @@ -0,0 +1,8 @@ +{ + "@id": "{{id}}", + "@type": "DiskStorage", + "volume": "/default-volume", + "default": false, + "readPermission": "{{read-permission}}", + "writePermission": "{{write-permission}}" +} \ No newline at end of file diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/Identity.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/Identity.scala index b1a18e5878..d433f1a07f 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/Identity.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/Identity.scala @@ -43,6 +43,7 @@ object Identity extends TestHelpers { object userPermissions { val UserWithNoPermissions = UserCredentials(genString(), genString(), testRealm) val UserWithPermissions = UserCredentials(genString(), genString(), testRealm) + val AdminUser = UserCredentials(genString(), genString(), testRealm) } object archives { @@ -99,6 +100,6 @@ object Identity extends TestHelpers { } lazy val allUsers = - userPermissions.UserWithNoPermissions :: userPermissions.UserWithPermissions :: acls.Marge :: archives.Tweety :: compositeviews.Jerry :: events.BugsBunny :: listings.Bob :: listings.Alice :: aggregations.Charlie :: aggregations.Rose :: orgs.Fry :: orgs.Leela :: projects.Bojack :: projects.PrincessCarolyn :: resources.Rick :: resources.Morty :: storages.Coyote :: views.ScoobyDoo :: mash.Radar :: supervision.Mickey :: Nil + userPermissions.AdminUser :: userPermissions.UserWithNoPermissions :: userPermissions.UserWithPermissions :: acls.Marge :: archives.Tweety :: compositeviews.Jerry :: events.BugsBunny :: listings.Bob :: listings.Alice :: aggregations.Charlie :: aggregations.Rose :: orgs.Fry :: orgs.Leela :: projects.Bojack :: projects.PrincessCarolyn :: resources.Rick :: resources.Morty :: storages.Coyote :: views.ScoobyDoo :: mash.Radar :: supervision.Mickey :: Nil } diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/iam/UserPermissionsSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/iam/UserPermissionsSpec.scala index 4adb4dca0b..0dc398244d 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/iam/UserPermissionsSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/iam/UserPermissionsSpec.scala @@ -3,13 +3,30 @@ package ch.epfl.bluebrain.nexus.tests.iam import akka.http.scaladsl.model.StatusCodes import ch.epfl.bluebrain.nexus.delta.kernel.utils.UrlUtils.encode import ch.epfl.bluebrain.nexus.tests.BaseSpec -import ch.epfl.bluebrain.nexus.tests.Identity.userPermissions.{UserWithNoPermissions, UserWithPermissions} -import ch.epfl.bluebrain.nexus.tests.iam.types.Permission.Resources +import ch.epfl.bluebrain.nexus.tests.Identity.userPermissions.{AdminUser, UserWithNoPermissions, UserWithPermissions} +import ch.epfl.bluebrain.nexus.tests.iam.types.Permission +import ch.epfl.bluebrain.nexus.tests.iam.types.Permission.{Organizations, Resources} +import io.circe.Json +import org.scalactic.source.Position class UserPermissionsSpec extends BaseSpec { val org, project = genId() + override def beforeAll(): Unit = { + super.beforeAll() + val result = for { + _ <- permissionDsl.addPermissions(StorageReadPermission, StorageWritePermission) + _ <- aclDsl.addPermission("/", AdminUser, Organizations.Create) + _ <- adminDsl.createOrganization(org, "UserPermissionsSpec organisation", AdminUser) + _ <- adminDsl.createProject(org, project, adminDsl.projectPayload(), AdminUser) + _ <- aclDsl.addPermission("/", AdminUser, Permission.Storages.Write) + _ <- createStorage(StorageId, StorageReadPermission, StorageWritePermission) + } yield succeed + + result.accepted + () + } private def urlFor(permission: String, project: String) = s"/user/permissions/$project?permission=${encode(permission)}" @@ -27,4 +44,58 @@ class UserPermissionsSpec extends BaseSpec { } } yield succeed } + + val StorageId = "https://bluebrain.github.io/nexus/vocabulary/storage1" + val StorageReadPermission = Permission("dan-storage", "read") + val StorageWritePermission = Permission("dan-storage", "write") + + private def storageUrlFor(project: String, storageId: String, typ: String): String = { + s"/user/permissions/$project?storage=${encode(storageId)}&type=$typ" + } + + "if a user does not have read permission for a storage, 403 should be returned" in { + deltaClient.head(storageUrlFor(s"$org/$project", StorageId, "read"), UserWithNoPermissions) { response => + response.status shouldBe StatusCodes.Forbidden + } + } + + "if a user has read permission for a storage, 204 should be returned" in { + for { + _ <- aclDsl.addPermission(s"/$org/$project", UserWithPermissions, StorageReadPermission) + _ <- deltaClient.head(storageUrlFor(s"$org/$project", StorageId, "read"), UserWithPermissions) { response => + response.status shouldBe StatusCodes.NoContent + } + } yield succeed + } + + "if a user does not have write permission for a storage, 403 should be returned" in { + deltaClient.head(storageUrlFor(s"$org/$project", StorageId, "write"), UserWithNoPermissions) { response => + response.status shouldBe StatusCodes.Forbidden + } + } + + "if a user has write permission for a storage, 204 should be returned" in { + for { + _ <- aclDsl.addPermission(s"/$org/$project", UserWithPermissions, StorageWritePermission) + _ <- deltaClient.head(storageUrlFor(s"$org/$project", StorageId, "write"), UserWithPermissions) { response => + response.status shouldBe StatusCodes.NoContent + } + } yield succeed + } + + private def createStorage(id: String, readPermission: Permission, writePermission: Permission)(implicit + pos: Position + ) = { + val payload = jsonContentOf( + "/kg/storages/disk-perms-parameterised.json", + "id" -> id, + "read-permission" -> readPermission.value, + "write-permission" -> writePermission.value + ) + deltaClient.post[Json](s"/storages/$org/$project", payload, AdminUser) { (_, response) => + withClue("creation of storage failed: ") { + response.status shouldEqual StatusCodes.Created + } + } + } }