Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Storage permission endpoint #4316

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`) =>
authorizeForAsync(
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 @@ -33,14 +33,17 @@ 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.StoragePermissionProvider.AccessType
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.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}
Expand Down Expand Up @@ -94,6 +97,22 @@ class StoragePluginModule(priority: Int) extends ModuleDef {
)
}

make[StoragePermissionProvider].from { (storages: Storages) =>
shinyhappydan marked this conversation as resolved.
Show resolved Hide resolved
new StoragePermissionProvider {
override def permissionFor(id: IdSegmentRef, project: ProjectRef, read: AccessType): UIO[Permission] =
storages
.fetch(id, project)
.map(storage => storage.value.storageValue)
.map(storage =>
read match {
case Read => storage.readPermission
case Write => storage.writePermission
}
)
.hideErrorsWith(_ => new RuntimeException("bob"))
shinyhappydan marked this conversation as resolved.
Show resolved Hide resolved
}
}

make[StoragesStatistics].from {
(
client: ElasticSearchClient,
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 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)
}
shinyhappydan marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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 {
shinyhappydan marked this conversation as resolved.
Show resolved Hide resolved

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
}
}
}
}