Skip to content

Commit

Permalink
Provision the project against the identities endpoint (#5063)
Browse files Browse the repository at this point in the history
Co-authored-by: Simon Dumas <[email protected]>
  • Loading branch information
imsdu and Simon Dumas authored Jul 10, 2024
1 parent dec5893 commit e6fbd35
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 153 deletions.
Original file line number Diff line number Diff line change
@@ -1,33 +1,37 @@
package ch.epfl.bluebrain.nexus.delta.routes

import akka.http.scaladsl.server.Route
import akka.http.scaladsl.server.{Directive0, Route}
import cats.effect.IO
import cats.effect.unsafe.implicits._
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution
import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering
import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck
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.identities.model.Caller
import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller._
import ch.epfl.bluebrain.nexus.delta.sdk.model.BaseUri
import kamon.instrumentation.akka.http.TracingDirectives.operationName
import ch.epfl.bluebrain.nexus.delta.sdk.provisioning.ProjectProvisioning

/**
* The identities routes
*/
class IdentitiesRoutes(identities: Identities, aclCheck: AclCheck)(implicit
class IdentitiesRoutes(identities: Identities, aclCheck: AclCheck, projectProvisioning: ProjectProvisioning)(implicit
baseUri: BaseUri,
cr: RemoteContextResolution,
ordering: JsonKeyOrdering
) extends AuthDirectives(identities, aclCheck) {

import baseUri.prefixSegment
private def provisionProject(implicit caller: Caller): Directive0 = onSuccess(
projectProvisioning(caller.subject).unsafeToFuture()
)

def routes: Route = {
baseUriPrefix(baseUri.prefix) {
(pathPrefix("identities") & pathEndOrSingleSlash) {
operationName(s"/$prefixSegment/identities") {
(extractCaller & get) { caller =>
(extractCaller & get) { implicit caller =>
provisionProject.apply {
emit(IO.pure(caller))
}
}
Expand All @@ -44,7 +48,8 @@ object IdentitiesRoutes {
*/
def apply(
identities: Identities,
aclCheck: AclCheck
aclCheck: AclCheck,
projectProvisioning: ProjectProvisioning
)(implicit baseUri: BaseUri, cr: RemoteContextResolution, ordering: JsonKeyOrdering): Route =
new IdentitiesRoutes(identities, aclCheck).routes
new IdentitiesRoutes(identities, aclCheck, projectProvisioning).routes
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package ch.epfl.bluebrain.nexus.delta.routes
import akka.http.scaladsl.model._
import akka.http.scaladsl.server._
import cats.data.OptionT
import cats.effect.unsafe.implicits._
import cats.implicits._
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder
Expand All @@ -26,8 +25,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.resources.{read
import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ProjectRejection.ProjectNotFound
import ch.epfl.bluebrain.nexus.delta.sdk.projects.model._
import ch.epfl.bluebrain.nexus.delta.sdk.projects.{Projects, ProjectsConfig, ProjectsStatistics}
import ch.epfl.bluebrain.nexus.delta.sdk.provisioning.ProjectProvisioning
import kamon.instrumentation.akka.http.TracingDirectives.operationName

/**
* The project routes
Expand All @@ -44,8 +41,7 @@ final class ProjectsRoutes(
identities: Identities,
aclCheck: AclCheck,
projects: Projects,
projectsStatistics: ProjectsStatistics,
projectProvisioning: ProjectProvisioning
projectsStatistics: ProjectsStatistics
)(implicit
baseUri: BaseUri,
config: ProjectsConfig,
Expand All @@ -55,8 +51,6 @@ final class ProjectsRoutes(
) extends AuthDirectives(identities, aclCheck)
with CirceUnmarshalling {

import baseUri.prefixSegment

implicit val paginationConfig: PaginationConfig = config.pagination

private def projectsSearchParams(implicit caller: Caller): Directive1[ProjectSearchParams] = {
Expand All @@ -73,10 +67,6 @@ final class ProjectsRoutes(
}
}

private def provisionProject(implicit caller: Caller): Directive0 = onSuccess(
projectProvisioning(caller.subject).unsafeToFuture()
)

private def revisionParam: Directive[Tuple1[Int]] = parameter("rev".as[Int])

def routes: Route =
Expand All @@ -85,95 +75,89 @@ final class ProjectsRoutes(
extractCaller { implicit caller =>
concat(
// List projects
(get & pathEndOrSingleSlash & extractUri & fromPaginated & provisionProject & projectsSearchParams &
(get & pathEndOrSingleSlash & extractUri & fromPaginated & projectsSearchParams &
sort[Project]) { (uri, pagination, params, order) =>
operationName(s"$prefixSegment/projects") {
implicit val searchJsonLdEncoder: JsonLdEncoder[SearchResults[ProjectResource]] =
searchResultsJsonLdEncoder(Project.context, pagination, uri)
implicit val searchJsonLdEncoder: JsonLdEncoder[SearchResults[ProjectResource]] =
searchResultsJsonLdEncoder(Project.context, pagination, uri)

emit(projects.list(pagination, params, order).widen[SearchResults[ProjectResource]])
}
emit(projects.list(pagination, params, order).widen[SearchResults[ProjectResource]])
},
projectRef.apply { project =>
concat(
operationName(s"$prefixSegment/projects/{org}/{project}") {
concat(
(put & pathEndOrSingleSlash) {
parameter("rev".as[Int].?) {
case None =>
// Create project
authorizeFor(project, CreateProjects).apply {
entity(as[ProjectFields]) { fields =>
emit(
StatusCodes.Created,
projects.create(project, fields).mapValue(_.metadata).attemptNarrow[ProjectRejection]
)
}
concat(
(put & pathEndOrSingleSlash) {
parameter("rev".as[Int].?) {
case None =>
// Create project
authorizeFor(project, CreateProjects).apply {
entity(as[ProjectFields]) { fields =>
emit(
StatusCodes.Created,
projects.create(project, fields).mapValue(_.metadata).attemptNarrow[ProjectRejection]
)
}
case Some(rev) =>
// Update project
authorizeFor(project, WriteProjects).apply {
entity(as[ProjectFields]) { fields =>
emit(
projects
.update(project, rev, fields)
.mapValue(_.metadata)
.attemptNarrow[ProjectRejection]
)
}
}
case Some(rev) =>
// Update project
authorizeFor(project, WriteProjects).apply {
entity(as[ProjectFields]) { fields =>
emit(
projects
.update(project, rev, fields)
.mapValue(_.metadata)
.attemptNarrow[ProjectRejection]
)
}
}
},
(get & pathEndOrSingleSlash) {
parameter("rev".as[Int].?) {
case Some(rev) => // Fetch project at specific revision
}
}
},
(get & pathEndOrSingleSlash) {
parameter("rev".as[Int].?) {
case Some(rev) => // Fetch project at specific revision
authorizeFor(project, ReadProjects).apply {
emit(projects.fetchAt(project, rev).attemptNarrow[ProjectRejection])
}
case None => // Fetch project
emitOrFusionRedirect(
project,
authorizeFor(project, ReadProjects).apply {
emit(projects.fetchAt(project, rev).attemptNarrow[ProjectRejection])
}
case None => // Fetch project
emitOrFusionRedirect(
project,
authorizeFor(project, ReadProjects).apply {
emit(projects.fetch(project).attemptNarrow[ProjectRejection])
}
)
}
},
// Deprecate/delete project
(delete & pathEndOrSingleSlash) {
parameters("rev".as[Int], "prune".?(false)) {
case (rev, true) =>
authorizeFor(project, DeleteProjects).apply {
emit(projects.delete(project, rev).mapValue(_.metadata).attemptNarrow[ProjectRejection])
emit(projects.fetch(project).attemptNarrow[ProjectRejection])
}
case (rev, false) =>
authorizeFor(project, WriteProjects).apply {
emit(projects.deprecate(project, rev).mapValue(_.metadata).attemptNarrow[ProjectRejection])
}
}
)
}
)
},
},
// Deprecate/delete project
(delete & pathEndOrSingleSlash) {
parameters("rev".as[Int], "prune".?(false)) {
case (rev, true) =>
authorizeFor(project, DeleteProjects).apply {
emit(projects.delete(project, rev).mapValue(_.metadata).attemptNarrow[ProjectRejection])
}
case (rev, false) =>
authorizeFor(project, WriteProjects).apply {
emit(projects.deprecate(project, rev).mapValue(_.metadata).attemptNarrow[ProjectRejection])
}
}
}
),
(pathPrefix("undeprecate") & put & revisionParam) { revision =>
authorizeFor(project, WriteProjects).apply {
emit(projects.undeprecate(project, revision).attemptNarrow[ProjectRejection])
}
},
operationName(s"$prefixSegment/projects/{org}/{project}/statistics") {
// Project statistics
(pathPrefix("statistics") & get & pathEndOrSingleSlash) {
authorizeFor(project, ReadResources).apply {
val stats = projectsStatistics.get(project)
emit(
OptionT(stats).toRight[ProjectRejection](ProjectNotFound(project)).value
)
}
// Project statistics
(pathPrefix("statistics") & get & pathEndOrSingleSlash) {
authorizeFor(project, ReadResources).apply {
val stats = projectsStatistics.get(project)
emit(
OptionT(stats).toRight[ProjectRejection](ProjectNotFound(project)).value
)
}
}
)
},
// list projects for an organization
(get & label & pathEndOrSingleSlash & extractUri & fromPaginated & provisionProject & projectsSearchParams &
(get & label & pathEndOrSingleSlash & extractUri & fromPaginated & projectsSearchParams &
sort[Project]) { (organization, uri, pagination, params, order) =>
implicit val searchJsonLdEncoder: JsonLdEncoder[SearchResults[ProjectResource]] =
searchResultsJsonLdEncoder(Project.context, pagination, uri)
Expand All @@ -197,15 +181,14 @@ object ProjectsRoutes {
identities: Identities,
aclCheck: AclCheck,
projects: Projects,
projectsStatistics: ProjectsStatistics,
projectProvisioning: ProjectProvisioning
projectsStatistics: ProjectsStatistics
)(implicit
baseUri: BaseUri,
config: ProjectsConfig,
cr: RemoteContextResolution,
ordering: JsonKeyOrdering,
fusionConfig: FusionConfig
): Route =
new ProjectsRoutes(identities, aclCheck, projects, projectsStatistics, projectProvisioning).routes
new ProjectsRoutes(identities, aclCheck, projects, projectsStatistics).routes

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.auth.{AuthTokenProvider, OpenIdAuthServ
import ch.epfl.bluebrain.nexus.delta.sdk.http.HttpClient
import ch.epfl.bluebrain.nexus.delta.sdk.identities.{Identities, IdentitiesImpl}
import ch.epfl.bluebrain.nexus.delta.sdk.model.BaseUri
import ch.epfl.bluebrain.nexus.delta.sdk.provisioning.ProjectProvisioning
import ch.epfl.bluebrain.nexus.delta.sdk.realms.Realms
import izumi.distage.model.definition.{Id, ModuleDef}

Expand Down Expand Up @@ -48,10 +49,11 @@ object IdentitiesModule extends ModuleDef {
(
identities: Identities,
aclCheck: AclCheck,
projectProvisioning: ProjectProvisioning,
baseUri: BaseUri,
cr: RemoteContextResolution @Id("aggregate"),
ordering: JsonKeyOrdering
) => new IdentitiesRoutes(identities, aclCheck)(baseUri, cr, ordering)
) => new IdentitiesRoutes(identities, aclCheck, projectProvisioning)(baseUri, cr, ordering)

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,12 @@ object ProjectsModule extends ModuleDef {
aclCheck: AclCheck,
projects: Projects,
projectsStatistics: ProjectsStatistics,
projectProvisioning: ProjectProvisioning,
baseUri: BaseUri,
cr: RemoteContextResolution @Id("aggregate"),
ordering: JsonKeyOrdering,
fusionConfig: FusionConfig
) =>
new ProjectsRoutes(identities, aclCheck, projects, projectsStatistics, projectProvisioning)(
new ProjectsRoutes(identities, aclCheck, projects, projectsStatistics)(
baseUri,
config.projects,
cr,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.model.headers.{Accept, BasicHttpCredentials, OAuth2BearerToken}
import akka.http.scaladsl.server.Directives.handleExceptions
import akka.http.scaladsl.server.Route
import cats.effect.{IO, Ref}
import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclSimpleCheck
import ch.epfl.bluebrain.nexus.delta.sdk.identities.IdentitiesDummy
import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.RdfExceptionHandler
import ch.epfl.bluebrain.nexus.delta.sdk.provisioning.ProjectProvisioning
import ch.epfl.bluebrain.nexus.delta.sdk.utils.BaseRouteSpec
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group}
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group, Subject}

class IdentitiesRoutesSpec extends BaseRouteSpec {

Expand All @@ -20,36 +23,45 @@ class IdentitiesRoutesSpec extends BaseRouteSpec {

private val aclCheck = AclSimpleCheck().accepted

private val refSubjects = Ref.unsafe[IO, Set[Subject]](Set.empty[Subject])

private val projectProvisioning: ProjectProvisioning =
(subject: Identity.Subject) => refSubjects.update(_ + subject)

private val route = Route.seal(
handleExceptions(RdfExceptionHandler.apply) {
IdentitiesRoutes(identities, aclCheck)
IdentitiesRoutes(identities, aclCheck, projectProvisioning)
}
)

"The identity routes" should {
"return forbidden" in {
Get("/v1/identities") ~> addCredentials(OAuth2BearerToken("unknown")) ~> route ~> check {
status shouldEqual StatusCodes.Unauthorized
refSubjects.get.accepted shouldBe empty
}
}

"return unauthorized" in {
Get("/v1/identities") ~> addCredentials(BasicHttpCredentials("fail")) ~> route ~> check {
status shouldEqual StatusCodes.Unauthorized
refSubjects.get.accepted shouldBe empty
}
}

"return anonymous" in {
Get("/v1/identities") ~> Accept(`*/*`) ~> route ~> check {
status shouldEqual StatusCodes.OK
response.asJson should equalIgnoreArrayOrder(jsonContentOf("identities/anonymous.json"))
refSubjects.get.accepted should contain(Anonymous)
}
}

"return all identities" in {
Get("/v1/identities") ~> Accept(`*/*`) ~> addCredentials(OAuth2BearerToken("alice")) ~> route ~> check {
status shouldEqual StatusCodes.OK
response.asJson should equalIgnoreArrayOrder(jsonContentOf("identities/alice.json"))
refSubjects.get.accepted should contain(alice)
}
}
}
Expand Down
Loading

0 comments on commit e6fbd35

Please sign in to comment.