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

Add user permissions endpoint #4296

Merged
merged 11 commits into from
Sep 26, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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.sdk.acls.AclCheck
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 monix.execution.Scheduler

/**
* The permissions routes.
shinyhappydan marked this conversation as resolved.
Show resolved Hide resolved
*
* @param identities
* the identities operations bundle
* @param permissions
* the permissions operations bundle
* @param aclCheck
* verify the acls for users
*/
final class UserPermissionsRoutes(identities: Identities, aclCheck: AclCheck)(implicit
baseUri: BaseUri,
s: Scheduler,
) extends AuthDirectives(identities, aclCheck)
with CirceUnmarshalling {

def routes: Route =
baseUriPrefix(baseUri.prefix) {
pathPrefix("user") {
pathPrefix("permissions") {
projectRef { project =>
extractCaller { implicit caller =>
(head & permission) { permission =>
authorizeFor(project, permission)(caller) {
complete(StatusCodes.NoContent)
}
}
}
}
}
}
}
}
shinyhappydan marked this conversation as resolved.
Show resolved Hide resolved

object UserPermissionsRoutes {
def apply(identities: Identities, aclCheck: AclCheck)(implicit
baseUri: BaseUri,
s: Scheduler,
): Route =
new UserPermissionsRoutes(identities, aclCheck: AclCheck).routes

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import ch.epfl.bluebrain.nexus.delta.config.AppConfig
import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.contexts
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteContextResolution}
import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering
import ch.epfl.bluebrain.nexus.delta.routes.AclsRoutes
import ch.epfl.bluebrain.nexus.delta.routes.{AclsRoutes, UserPermissionsRoutes}
import ch.epfl.bluebrain.nexus.delta.sdk._
import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.AclEvent
import ch.epfl.bluebrain.nexus.delta.sdk.acls.{AclCheck, Acls, AclsImpl}
Expand Down Expand Up @@ -71,6 +71,15 @@ object AclsModule extends ModuleDef {
} yield RemoteContextResolution.fixed(contexts.acls -> aclsCtx, contexts.aclsMetadata -> aclsMetaCtx)
)

make[UserPermissionsRoutes].from { (identities: Identities, aclCheck: AclCheck, baseUri: BaseUri,
s: Scheduler) =>
new UserPermissionsRoutes(identities, aclCheck)(baseUri, s)
}

many[PriorityRoute].add { (route: UserPermissionsRoutes) =>
shinyhappydan marked this conversation as resolved.
Show resolved Hide resolved
PriorityRoute(pluginsMaxPriority + 100, route.routes, requiresStrictEntity = true)
}

many[PriorityRoute].add { (route: AclsRoutes) =>
PriorityRoute(pluginsMaxPriority + 5, route.routes, requiresStrictEntity = true)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{JsonLdFormat, QueryParamsU
import ch.epfl.bluebrain.nexus.delta.sdk.model.IdSegment.StringSegment
import ch.epfl.bluebrain.nexus.delta.sdk.model._
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.PaginationConfig
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission
import ch.epfl.bluebrain.nexus.delta.sdk.{IndexingMode, OrderingFields}
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag
Expand Down Expand Up @@ -106,6 +107,13 @@ trait UriDirectives extends QueryParamsUnmarshalling {
ProjectRef(org, proj)
}

def permission: Directive1[Permission] = parameter("permission").flatMap { value =>
shinyhappydan marked this conversation as resolved.
Show resolved Hide resolved
Permission(value) match {
case Left(err) => reject(validationRejection(err.getMessage))
case Right(permission) => provide(permission)
}
}

/**
* This directive passes when the query parameter specified is not present
*
Expand Down
65 changes: 55 additions & 10 deletions tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/HttpClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import akka.http.scaladsl.model.HttpCharsets._
import akka.http.scaladsl.model.HttpMethods._
import akka.http.scaladsl.model.Multipart.FormData
import akka.http.scaladsl.model.Multipart.FormData.BodyPart
import akka.http.scaladsl.model.headers.{`Accept-Encoding`, Accept, Authorization, HttpEncodings}
import akka.http.scaladsl.model.headers.{Accept, Authorization, HttpEncodings, `Accept-Encoding`}
import akka.http.scaladsl.model._
import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller
import akka.http.scaladsl.{Http, HttpExt}
Expand Down Expand Up @@ -36,6 +36,11 @@ class HttpClient private (baseUrl: Uri, httpExt: HttpExt)(implicit as: ActorSyst
def apply(req: HttpRequest): Task[HttpResponse] =
Task.deferFuture(httpExt.singleRequest(req))

def head(url: Uri, identity: Identity)(assertResponse: HttpResponse => Assertion): Task[Assertion] = {
val req = HttpRequest(HEAD, s"$baseUrl$url", headers = identityHeader(identity).toList)
Task.deferFuture(httpExt.singleRequest(req)).map(assertResponse)
}
shinyhappydan marked this conversation as resolved.
Show resolved Hide resolved

def run[A](req: HttpRequest)(implicit um: FromEntityUnmarshaller[A]): Task[(A, HttpResponse)] =
Task.deferFuture(httpExt.singleRequest(req)).flatMap { res =>
Task.deferFuture(um.apply(res.entity)).map(a => (a, res))
Expand Down Expand Up @@ -127,6 +132,10 @@ class HttpClient private (baseUrl: Uri, httpExt: HttpExt)(implicit as: ActorSyst
)(implicit um: FromEntityUnmarshaller[A]): Task[Assertion] =
requestAssert(GET, url, None, identity, extraHeaders)(assertResponse)

// def head(url: String, identity: Identity)(assertResponse: HttpResponse => Assertion): Task[Assertion] = {
// requestAssertHead(url, identity)(assertResponse)
// }

def getJson[A](url: String, identity: Identity)(implicit um: FromEntityUnmarshaller[A]): Task[A] = {
def onFail(e: Throwable) =
throw new IllegalStateException(
Expand All @@ -141,6 +150,34 @@ class HttpClient private (baseUrl: Uri, httpExt: HttpExt)(implicit as: ActorSyst
)(implicit um: FromEntityUnmarshaller[A]): Task[Assertion] =
requestAssert(DELETE, url, None, identity, extraHeaders)(assertResponse)

// def requestAssertHead(url: String, identity: Identity)(assertResponse: HttpResponse => Assertion): Task[Assertion] = {
// def buildClue(response: HttpResponse) =
// s"""
// |Endpoint: ${HEAD.value} $url
// |Identity: $identity
// |Token: ${Option(tokensMap.get(identity)).map(_.credentials.token()).getOrElse("None")}
// |Status code: ${response.status}
// |Response:
// |$a
// |""".stripMargin
//
// def onFail(e: Throwable) =
// fail(
// s"Something went wrong while processing the response for url: ${HEAD.value} $url with identity $identity",
// e
// )
//
// requestJson(
// method,
// url,
// body,
// identity,
// (a: A, response: HttpResponse) => assertResponse(a, response) withClue buildClue(a, response),
// onFail,
// extraHeaders
// )
// }

def requestAssert[A](
method: HttpMethod,
url: String,
Expand Down Expand Up @@ -212,6 +249,22 @@ class HttpClient private (baseUrl: Uri, httpExt: HttpExt)(implicit as: ActorSyst
extraHeaders
)

// def requestHead[R](url: String, identity: Identity, handleError: Throwable => Assertion): Task[Assertion] = {
shinyhappydan marked this conversation as resolved.
Show resolved Hide resolved
// apply(HttpRequest())
// }

private def identityHeader(identity: Identity): Option[HttpHeader] = {
identity match {
case Anonymous => None
case _ =>
Some(Option(tokensMap.get(identity)).getOrElse(
throw new IllegalArgumentException(
"The provided user has not been properly initialized, please add it to Identity.allUsers."
)
))
}
}

def request[A, B, R](
method: HttpMethod,
url: String,
Expand All @@ -226,15 +279,7 @@ class HttpClient private (baseUrl: Uri, httpExt: HttpExt)(implicit as: ActorSyst
HttpRequest(
method = method,
uri = s"$baseUrl$url",
headers = identity match {
case Anonymous => extraHeaders
case _ =>
extraHeaders :+ Option(tokensMap.get(identity)).getOrElse(
throw new IllegalArgumentException(
"The provided user has not been properly initialized, please add it to Identity.allUsers."
)
)
},
headers = extraHeaders ++ identityHeader(identity),
entity = body.fold(HttpEntity.Empty)(toEntity)
)
).flatMap { res =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ object Identity extends TestHelpers {
val Marge = UserCredentials(genString(), genString(), testRealm)
}

object userPermissions {
val UserWithNoPermissions = UserCredentials(genString(), genString(), testRealm)
val UserWithPermissions = UserCredentials(genString(), genString(), testRealm)
}

object archives {
val Tweety = UserCredentials(genString(), genString(), testRealm)
}
Expand Down Expand Up @@ -94,6 +99,6 @@ object Identity extends TestHelpers {
}

lazy val allUsers =
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.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
@@ -0,0 +1,27 @@
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

class UserPermissionsSpec extends BaseSpec {

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

"if a user does not have a permission, 403 should be returned" in {
deltaClient.head(urlFor("resources/read", "org/project"), UserWithNoPermissions) { response =>
shinyhappydan marked this conversation as resolved.
Show resolved Hide resolved
response.status shouldBe StatusCodes.Forbidden
}
}

"if a user has a permission, 204 should be returned" in {
for {
_ <- aclDsl.addPermission("/org/project", UserWithPermissions, Resources.Read)
_ <- deltaClient.head(urlFor("resources/read", "org/project"), UserWithPermissions) { response =>
response.status shouldBe StatusCodes.NoContent
}
} yield succeed
}
}
Loading