Skip to content

Commit

Permalink
Working storage permission endpoint (refactoring / fixes needed)
Browse files Browse the repository at this point in the history
  • Loading branch information
shinyhappydan committed Sep 29, 2023
1 parent b6faad4 commit 7408256
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -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) {
Expand All @@ -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)
}
}
}
)
}
}
}
Expand All @@ -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
}
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,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}
Expand Down Expand Up @@ -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,
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)
}

/**
* Check whether [[Caller]] is the configured service account.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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]

}
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 @@ -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 {
Expand Down Expand Up @@ -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

}
Original file line number Diff line number Diff line change
Expand Up @@ -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)}"

Expand All @@ -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
}
}
}
}

0 comments on commit 7408256

Please sign in to comment.