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,52 @@
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 ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission

/**
* The user permissions routes. Used for checking whether the current logged in user has certain permissions.
*
* @param identities
* the identities operations bundle
* @param aclCheck
* verify the acls for users
*/
final class UserPermissionsRoutes(identities: Identities, aclCheck: AclCheck)(implicit
baseUri: BaseUri
) extends AuthDirectives(identities, aclCheck)
with CirceUnmarshalling {

def routes: Route =
baseUriPrefix(baseUri.prefix) {
pathPrefix("user") {
pathPrefix("permissions") {
projectRef { project =>
extractCaller { implicit caller =>
head {
parameter("permission".as[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
): Route =
new UserPermissionsRoutes(identities, aclCheck: AclCheck).routes
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package ch.epfl.bluebrain.nexus.delta.wiring

import akka.http.scaladsl.server.RouteConcatenation
import cats.effect.Clock
import ch.epfl.bluebrain.nexus.delta.Main.pluginsMaxPriority
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,8 +72,16 @@ object AclsModule extends ModuleDef {
} yield RemoteContextResolution.fixed(contexts.acls -> aclsCtx, contexts.aclsMetadata -> aclsMetaCtx)
)

many[PriorityRoute].add { (route: AclsRoutes) =>
PriorityRoute(pluginsMaxPriority + 5, route.routes, requiresStrictEntity = true)
make[UserPermissionsRoutes].from { (identities: Identities, aclCheck: AclCheck, baseUri: BaseUri) =>
new UserPermissionsRoutes(identities, aclCheck)(baseUri)
}

many[PriorityRoute].add { (alcs: AclsRoutes, userPermissions: UserPermissionsRoutes) =>
PriorityRoute(
pluginsMaxPriority + 5,
RouteConcatenation.concat(alcs.routes, userPermissions.routes),
requiresStrictEntity = true
)
}
}
// $COVERAGE-ON$
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.model.Permission
import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.{ApiMappings, ProjectContext}
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label
Expand Down Expand Up @@ -90,6 +91,14 @@ trait QueryParamsUnmarshalling {
}
}

implicit def permissionFromStringUnmarshaller: FromStringUnmarshaller[Permission] =
Unmarshaller.strict[String, Permission] { string =>
Permission(string) match {
case Right(value) => value
case Left(err) => throw new IllegalArgumentException(err.getMessage)
}
}

/**
* Unmarsaller to transform an Iri to a Subject
*/
Expand Down
52 changes: 34 additions & 18 deletions docs/src/main/paradox/docs/delta/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* @ref:[Permissions](permissions-api.md)
* @ref:[Realms](realms-api.md)
* @ref:[ACLs](acls-api.md)
* @ref:[User Permissions](user-permissions-api.md)
* @ref:[Organizations](orgs-api.md)
* @ref:[Projects](projects-api.md)
* @ref:[Quotas](quotas.md)
Expand All @@ -31,24 +32,28 @@

# API Reference

Nexus Delta exposes a RESTful interface over HTTP(S) for synchronous communication. The generally adopted transport
format is JSON based, specifically @link:[JSON-LD](https://json-ld.org/){ open=new }. However, other response formats are supported through @ref:[Content-Negotiation](content-negotiation.md)
Nexus Delta exposes a RESTful interface over HTTP(S) for synchronous communication. The generally adopted transport
format is JSON based, specifically @link:[JSON-LD](https://json-ld.org/){ open=new }. However, other response formats
are supported through @ref:[Content-Negotiation](content-negotiation.md)

The API provides access and management of several primary resource types.

## Identities

Identities endpoint can be used to fetch user identities.

@ref:[Operations on identities](identities.md)

## Permissions

A permission is the basic unit to provide a way to limit applications' access to sensitive information.

@ref:[Operations on permisions](permissions-api.md)

## Realms
A realm provides with the necessary information to perform authentication against a certain
@link:[OIDC](https://en.wikipedia.org/wiki/OpenID_Connect){ open=new } provider .
## Realms

A realm provides with the necessary information to perform authentication against a certain
@link:[OIDC](https://en.wikipedia.org/wiki/OpenID_Connect){ open=new } provider .

@ref:[Operations on realms](realms-api.md)

Expand All @@ -57,36 +62,45 @@ A realm provides with the necessary information to perform authentication agains
In order to restrict applications' access to data by placing restrictions on them, three parameters are important:

- permission: the value used to limit a client (user, group) access to resources.
- identity: a client identity reference, e.g. a certain user, a group, an anonymous user or someone who is
- identity: a client identity reference, e.g. a certain user, a group, an anonymous user or someone who is
authenticated to a certain realm.
- path: the location where to apply the restrictions

An ACL defines the set of **permissions** that certain **identities** have on a concrete **path**.

@ref:[Operations on ACLs](acls-api.md)

## User Permissions

## Organizations
ACLs are the permission model used in Delta. Sometimes it can be more convenient to ask more basic questions about
whether a user has a permission, rather than trying to determine this yourself from the ACLs API. This is what the user
permissions API is for

@ref:[Operations on User Permissions](user-permissions-api.md)

## Organizations

The top-level grouping resource in the platform, called organization

@ref:[Operations on organizations](orgs-api.md)

## Projects

The 2nd level grouping resources in the platform, called project. Projects provide isolation of ACLs, resource
The 2nd level grouping resources in the platform, called project. Projects provide isolation of ACLs, resource
resolution and indices (ElasticSearch index and Blazegraph namespace).

@ref:[Operations on projects](projects-api.md)

## Quotas

Defines the maximum number of resources and events that can exist in a certain scope.

@ref:[Operations on quotas](quotas.md)

## Schemas

A schema is a resource which defines a set of rules and constrains using @link:[SHACL](https://www.w3.org/TR/shacl/){ open=new }.
A schema is a resource which defines a set of rules and constrains using @link:[SHACL](https://www.w3.org/TR/shacl/){
open=new }.

@ref:[Operations on schemas](schemas-api.md)

Expand All @@ -111,7 +125,8 @@ A view is a resource which defines the way indexing is applied to certain resour

## Storages

A storage is a resource which represents a backend where files are stored. It describes where and how files are created and retrieve.
A storage is a resource which represents a backend where files are stored. It describes where and how files are created
and retrieve.

@ref:[Operations on storages](storages-api.md)

Expand All @@ -123,26 +138,27 @@ A file is a binary attachment resource.

## Archives

An archive is a collection of resources stored inside an archive file. The archiving format chosen for this purpose is ZIP file.
An archive is a collection of resources stored inside an archive file. The archiving format chosen for this purpose is
ZIP file.

@ref:[Operations on archives](archives-api.md)

## Resource Lifecycle

Nexus Delta is build using the @link:[event sourcing](https://martinfowler.com/eaaDev/EventSourcing.html){ open=new }
Nexus Delta is build using the @link:[event sourcing](https://martinfowler.com/eaaDev/EventSourcing.html){ open=new }
approach. This strategy captures all changes to an application state as a sequence of events.

All resources in the system generally follow the very same lifecycle, as depicted in the diagram below. Every
All resources in the system generally follow the very same lifecycle, as depicted in the diagram below. Every
interaction with an API resource (creation, updates, state changes) is recorded into the system as revisions.

![Resource Lifecycle](assets/resources/lifecycle.png "Resource Lifecycle")

Data is never removed from the system, but rather is marked as deprecated. Depending on the type of resource, the
Data is never removed from the system, but rather is marked as deprecated. Depending on the type of resource, the
deprecation flag may have various semantics:

- **Organizations**: the resource itself and sub-resources cannot be updated. Views and resolvers contained within
- **Organizations**: the resource itself and sub-resources cannot be updated. Views and resolvers contained within
this organization will not be considered during indexing and resolution processes.
- **Projects**: the resource itself and sub-resources cannot be updated. Views and resolvers contained within this
- **Projects**: the resource itself and sub-resources cannot be updated. Views and resolvers contained within this
project will not be considered during indexing and resolution processes.
- **Schemas**: the resource itself cannot be updated and new data conformant to it cannot be created
- **Resolvers**: the resource itself will not be considered during the resolution process
Expand All @@ -151,8 +167,8 @@ deprecation flag may have various semantics:
- **Files**: attachments cannot be added/deleted
- **Data**: the resource itself cannot be updated

`Archives` resources are an exception. Those resources are ephemeral. They will be automatically removed from the
system after certain time. This time is configurable (config property `app.archives.cache-invalidate-after`) and it
`Archives` resources are an exception. Those resources are ephemeral. They will be automatically removed from the
system after certain time. This time is configurable (config property `app.archives.cache-invalidate-after`) and it
defaults to 5 hours.

Future policies may use this flag to determine if or when the deprecated data may be archived.
Expand Down
30 changes: 30 additions & 0 deletions docs/src/main/paradox/docs/delta/api/user-permissions-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

# User Permissions

A user of delta is given certain permissions. This is done using the @ref:[ACLs API](acls-api.md)

Sometimes for the sake of simplicity, it can be easier to ask whether the current user has a specific permission in a specific context. This is why the user permissions API exists

@@@ note { .warning }

The described endpoints are experimental and the responses structure might change in the future.

@@@

## Head

This operation determines whether the current logged in user has a specific permission in a specific context

```
HEAD /v1/user/permissions/{org_label}/{project_label}?permission={permission}
```

where
- `{permission}`: String - the permission to check

Request
: The request should have no body

Response
: The response will have a 204 (no content) status code if the user is authorised
: The response will have a 403 (forbidden) status code if the user is not authorised
6 changes: 6 additions & 0 deletions docs/src/main/paradox/docs/releases/v1.9-release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ It is now possible to perform ID resolution by providing only the identifier of

@ref:[More information](../delta/api/id-resolution.md)

### User Permissions

It is now possible to query whether the current logged in user has a specific permission in a specific context.

@ref:[More information](../delta/api/user-permissions-api.md)

## Nexus Fusion

TODO
Expand Down
Original file line number Diff line number Diff line change
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 @@ -212,6 +217,20 @@ class HttpClient private (baseUrl: Uri, httpExt: HttpExt)(implicit as: ActorSyst
extraHeaders
)

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 +245,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

}
Loading