Skip to content

Commit

Permalink
Storage permission endpoint (#4316)
Browse files Browse the repository at this point in the history
* Working storage permission endpoint (refactoring / fixes needed)

* use ADT rather than string hacking

* remove unnecessary admin user

* deal with errors better

* rename method

* move logic to a class

* scalafmt
  • Loading branch information
shinyhappydan authored Sep 30, 2023
1 parent b6faad4 commit eb9fde2
Show file tree
Hide file tree
Showing 10 changed files with 182 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ 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, IdSegment, IdSegmentRef}
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.StoragePermissionProvider
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.StoragePermissionProvider.AccessType
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission

/**
Expand All @@ -19,10 +23,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) {
Expand All @@ -31,11 +36,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".as[IdSegment], "type".as[AccessType]) { (storageId, `type`) =>
authorizeForIO(
AclAddress.fromProject(project),
storages.permissionFor(IdSegmentRef(storageId), project, `type`)
)(caller) {
complete(StatusCodes.NoContent)
}
}
}
)
}
}
}
Expand All @@ -45,8 +60,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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.operations.Storage
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.operations.remote.client.RemoteDiskStorageClient
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.routes.StoragesRoutes
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.schemas.{storage => storagesSchemaId}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.{StorageDeletionTask, Storages, StoragesStatistics}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.{StorageDeletionTask, StoragePermissionProviderImpl, Storages, StoragesStatistics}
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.rdf.utils.JsonKeyOrdering
Expand All @@ -33,7 +33,7 @@ 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.{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
Expand Down Expand Up @@ -94,6 +94,10 @@ class StoragePluginModule(priority: Int) extends ModuleDef {
)
}

make[StoragePermissionProvider].from { (storages: Storages) =>
new StoragePermissionProviderImpl(storages)
}

make[StoragesStatistics].from {
(
client: ElasticSearchClient,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ch.epfl.bluebrain.nexus.delta.plugins.storage.storages

import ch.epfl.bluebrain.nexus.delta.sdk.model.IdSegmentRef
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.StoragePermissionProvider
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.StoragePermissionProvider.AccessType.{Read, Write}
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission
import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef
import monix.bio.UIO

class StoragePermissionProviderImpl(storages: Storages) extends StoragePermissionProvider {
override def permissionFor(
id: IdSegmentRef,
project: ProjectRef,
accessType: StoragePermissionProvider.AccessType
): UIO[Permission] = {
storages
.fetch(id, project)
.map(storage => storage.value.storageValue)
.map(storage =>
accessType match {
case Read => storage.readPermission
case Write => storage.writePermission
}
)
.hideErrors
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import io.circe.{Encoder, JsonObject}
* a descriptive message as to why the rejection occurred
*/
sealed abstract class StorageRejection(val reason: String, val loggedDetails: Option[String] = None)
extends Product
extends Exception(reason)
with Product
with Serializable

object StorageRejection {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 authorizeForIO(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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, JsonLdCon
import ch.epfl.bluebrain.nexus.delta.sdk.implicits._
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.QueryParamsUnmarshalling.{IriBase, IriVocab}
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, IdSegment}
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.StoragePermissionProvider.AccessType
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission
import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.{ApiMappings, ProjectContext}
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject
Expand Down Expand Up @@ -99,6 +100,14 @@ trait QueryParamsUnmarshalling {
}
}

implicit def accessTypeFromStringUnmarshaller: FromStringUnmarshaller[AccessType] =
Unmarshaller.strict[String, AccessType] {
case "read" => AccessType.Read
case "write" => AccessType.Write
case string =>
throw new IllegalArgumentException(s"Access type can be either 'read' or 'write', received [$string]")
}

/**
* Unmarsaller to transform an Iri to a Subject
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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.StoragePermissionProvider.AccessType
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, accessType: AccessType): UIO[Permission]

}

object StoragePermissionProvider {
sealed trait AccessType
object AccessType {
case object Read extends AccessType
case object Write extends AccessType
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"@id": "{{id}}",
"@type": "DiskStorage",
"volume": "/default-volume",
"default": false,
"readPermission": "{{read-permission}}",
"writePermission": "{{write-permission}}"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,32 @@ 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
import ch.epfl.bluebrain.nexus.tests.iam.types.Permission.Resources
import ch.epfl.bluebrain.nexus.tests.{BaseSpec, Identity}
import io.circe.Json
import org.scalactic.source.Position

class UserPermissionsSpec extends BaseSpec {

val org, project = genId()
val org, project = genId()
val StorageId = "https://bluebrain.github.io/nexus/vocabulary/storage1"
val StorageReadPermission = Permission("s3-storage", "read")
val StorageWritePermission = Permission("s3-storage", "write")

override def beforeAll(): Unit = {
super.beforeAll()
val result = for {
_ <- permissionDsl.addPermissions(StorageReadPermission, StorageWritePermission)
_ <- adminDsl.createOrganization(org, "UserPermissionsSpec organisation", Identity.ServiceAccount)
_ <- adminDsl.createProject(org, project, adminDsl.projectPayload(), Identity.ServiceAccount)
_ <- createStorage(StorageId, StorageReadPermission, StorageWritePermission)
} yield succeed

result.accepted
()
}
private def urlFor(permission: String, project: String) =
s"/user/permissions/$project?permission=${encode(permission)}"

Expand All @@ -27,4 +45,54 @@ class UserPermissionsSpec extends BaseSpec {
}
} yield succeed
}

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, Identity.ServiceAccount) { (_, response) =>
withClue("creation of storage failed: ") {
response.status shouldEqual StatusCodes.Created
}
}
}
}

0 comments on commit eb9fde2

Please sign in to comment.