diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/IdentitiesRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/IdentitiesRoutes.scala index 755725974f..918f36c35f 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/IdentitiesRoutes.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/IdentitiesRoutes.scala @@ -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)) } } @@ -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 } diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ProjectsRoutes.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ProjectsRoutes.scala index fc875d97a6..3974d0d560 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ProjectsRoutes.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/ProjectsRoutes.scala @@ -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 @@ -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 @@ -44,8 +41,7 @@ final class ProjectsRoutes( identities: Identities, aclCheck: AclCheck, projects: Projects, - projectsStatistics: ProjectsStatistics, - projectProvisioning: ProjectProvisioning + projectsStatistics: ProjectsStatistics )(implicit baseUri: BaseUri, config: ProjectsConfig, @@ -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] = { @@ -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 = @@ -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) @@ -197,8 +181,7 @@ object ProjectsRoutes { identities: Identities, aclCheck: AclCheck, projects: Projects, - projectsStatistics: ProjectsStatistics, - projectProvisioning: ProjectProvisioning + projectsStatistics: ProjectsStatistics )(implicit baseUri: BaseUri, config: ProjectsConfig, @@ -206,6 +189,6 @@ object ProjectsRoutes { ordering: JsonKeyOrdering, fusionConfig: FusionConfig ): Route = - new ProjectsRoutes(identities, aclCheck, projects, projectsStatistics, projectProvisioning).routes + new ProjectsRoutes(identities, aclCheck, projects, projectsStatistics).routes } diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/IdentitiesModule.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/IdentitiesModule.scala index 607d47e916..1ffe8f7e94 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/IdentitiesModule.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/IdentitiesModule.scala @@ -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} @@ -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) } diff --git a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ProjectsModule.scala b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ProjectsModule.scala index 2af28c369d..0125c6c804 100644 --- a/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ProjectsModule.scala +++ b/delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/ProjectsModule.scala @@ -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, diff --git a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/IdentitiesRoutesSpec.scala b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/IdentitiesRoutesSpec.scala index 56e57c2ab8..c24f9d1bdc 100644 --- a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/IdentitiesRoutesSpec.scala +++ b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/IdentitiesRoutesSpec.scala @@ -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 { @@ -20,9 +23,14 @@ 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) } ) @@ -30,12 +38,14 @@ class IdentitiesRoutesSpec extends BaseRouteSpec { "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 } } @@ -43,6 +53,7 @@ class IdentitiesRoutesSpec extends BaseRouteSpec { Get("/v1/identities") ~> Accept(`*/*`) ~> route ~> check { status shouldEqual StatusCodes.OK response.asJson should equalIgnoreArrayOrder(jsonContentOf("identities/anonymous.json")) + refSubjects.get.accepted should contain(Anonymous) } } @@ -50,6 +61,7 @@ class IdentitiesRoutesSpec extends BaseRouteSpec { 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) } } } diff --git a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ProjectsRoutesSpec.scala b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ProjectsRoutesSpec.scala index a353f343a5..19027914b9 100644 --- a/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ProjectsRoutesSpec.scala +++ b/delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/ProjectsRoutesSpec.scala @@ -16,7 +16,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationRejecti import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.{projects => projectsPermissions, resources} import ch.epfl.bluebrain.nexus.delta.sdk.projects.model._ import ch.epfl.bluebrain.nexus.delta.sdk.projects.{ProjectsConfig, ProjectsImpl, ProjectsStatistics} -import ch.epfl.bluebrain.nexus.delta.sdk.provisioning.{AutomaticProvisioningConfig, ProjectProvisioning} import ch.epfl.bluebrain.nexus.delta.sdk.utils.BaseRouteSpec import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Subject, User} import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef} @@ -38,8 +37,6 @@ class ProjectsRoutesSpec extends BaseRouteSpec with BeforeAndAfterAll { private val orgUuid = UUID.randomUUID() - private val provisionedRealm = Label.unsafe("realm2") - private val userWithWritePermission = User("userWithWritePermission", Label.unsafe(genString())) private val userWithCreatePermission = User("userWithCreatePermission", Label.unsafe(genString())) private val userWithReadPermission = User("userWithReadPermission", Label.unsafe(genString())) @@ -48,9 +45,7 @@ class ProjectsRoutesSpec extends BaseRouteSpec with BeforeAndAfterAll { private val userWithReadSingleProjectPermission = User("userWithReadSingleProjectPermission", Label.unsafe(genString())) - private val superUser = User("superUser", Label.unsafe(genString())) - private val provisionedUser = User("user1", provisionedRealm) - private val invalidUser = User("!@#%^", provisionedRealm) + private val superUser = User("superUser", Label.unsafe(genString())) private val org1 = Label.unsafe("org1") private val org2 = Label.unsafe("org2") @@ -65,18 +60,6 @@ class ProjectsRoutesSpec extends BaseRouteSpec with BeforeAndAfterAll { case other => IO.raiseError(OrganizationNotFound(other)) } - private val provisioningConfig = AutomaticProvisioningConfig( - enabled = true, - permissions = Set(resources.read, resources.write, projectsPermissions.read), - enabledRealms = Map(Label.unsafe("realm2") -> Label.unsafe("users-org")), - ProjectFields( - Some("Auto provisioned project"), - ApiMappings.empty, - Some(PrefixIri.unsafe(iri"http://example.com/base/")), - Some(PrefixIri.unsafe(iri"http://example.com/vocab/")) - ) - ) - implicit private val projectsConfig: ProjectsConfig = ProjectsConfig(eventLogConfig, pagination, deletionConfig) private val projectStats = ProjectStatistics(10, 10, Instant.EPOCH) @@ -97,23 +80,19 @@ class ProjectsRoutesSpec extends BaseRouteSpec with BeforeAndAfterAll { AclAddress.Root, Set(projectsPermissions.create, projectsPermissions.read, projectsPermissions.write, projectsPermissions.delete) ), - (provisionedUser, AclAddress.Root, Set(projectsPermissions.read)), - (invalidUser, AclAddress.Root, Set.empty), (alice, AclAddress.Root, Set(projectsPermissions.create)), (userWithReadSingleProjectPermission, AclAddress.Project(ref), Set(projectsPermissions.read)) ) - private lazy val projects = + private lazy val projects = ProjectsImpl(fetchOrg, _ => IO.unit, ScopeInitializer.noop, defaultApiMappings, eventLogConfig, xas, clock) - private lazy val provisioning = - ProjectProvisioning(aclCheck.append, projects, provisioningConfig) - private lazy val routes = Route.seal( + + private lazy val routes = Route.seal( ProjectsRoutes( identities, aclCheck, projects, - projectsStatistics, - provisioning + projectsStatistics ) ) @@ -121,12 +100,12 @@ class ProjectsRoutesSpec extends BaseRouteSpec with BeforeAndAfterAll { val base = "https://localhost/base/" val vocab = "https://localhost/voc/" - val payload = jsonContentOf("projects/create.json", "description" -> desc, "base" -> base, "vocab" -> vocab) + private val payload = jsonContentOf("projects/create.json", "description" -> desc, "base" -> base, "vocab" -> vocab) - val payloadUpdated = + private val payloadUpdated = jsonContentOf("projects/create.json", "description" -> "New description", "base" -> base, "vocab" -> vocab) - val anotherPayload = jsonContentOf("projects/create.json", "description" -> desc) + private val anotherPayload = jsonContentOf("projects/create.json", "description" -> desc) "A project route" should { @@ -454,23 +433,6 @@ class ProjectsRoutesSpec extends BaseRouteSpec with BeforeAndAfterAll { } } - "provision project for user when listing" in { - Get("/v1/projects/users-org") ~> as(provisionedUser) ~> routes ~> check { - status shouldEqual StatusCodes.OK - response.asJson.asObject.value("_total").value.asNumber.value.toInt.value shouldEqual 1 - } - - Get("/v1/projects/users-org/user1") ~> as(provisionedUser) ~> routes ~> check { - status shouldEqual StatusCodes.OK - } - } - - "return error when failed to provision project" in { - Get("/v1/projects/users-org") ~> as(invalidUser) ~> routes ~> check { - status shouldEqual StatusCodes.InternalServerError - } - } - "redirect to fusion for the latest version if the Accept header is set to text/html" in { Get("/v1/projects/users-org/user1") ~> Accept(`text/html`) ~> routes ~> check { response.status shouldEqual StatusCodes.SeeOther diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/provisioning/ProjectProvisioning.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/provisioning/ProjectProvisioning.scala index c9a805aa2e..9415312163 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/provisioning/ProjectProvisioning.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/provisioning/ProjectProvisioning.scala @@ -1,7 +1,8 @@ package ch.epfl.bluebrain.nexus.delta.sdk.provisioning import cats.effect.IO - +import cats.implicits._ +import ch.epfl.bluebrain.nexus.delta.kernel.Logger import ch.epfl.bluebrain.nexus.delta.kernel.error.FormatError import ch.epfl.bluebrain.nexus.delta.sdk.acls.Acls import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.{Acl, AclAddress, AclRejection} @@ -12,7 +13,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ProjectRejection import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.ProjectRejection.ProjectAlreadyExists import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Subject, User} import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef} -import cats.implicits._ /** * Automatic project provisioning for users. @@ -31,6 +31,8 @@ trait ProjectProvisioning { object ProjectProvisioning { + private val logger = Logger[ProjectProvisioning] + /** * Rejection signalling that project provisioning failed. */ @@ -75,6 +77,7 @@ object ProjectProvisioning { ): IO[Unit] = { val acl = Acl(AclAddress.Project(projectRef), user -> provisioningConfig.permissions) for { + _ <- logger.info(s"Starting provisioning project for user ${user.subject}") _ <- appendAcls(acl) .recoverWith { case _: AclRejection.IncorrectRev => IO.unit @@ -89,6 +92,7 @@ object ProjectProvisioning { .void .recoverWith { case _: ProjectAlreadyExists => IO.unit } .adaptError { case r: ProjectRejection => UnableToCreateProject(r) } + _ <- logger.info(s"Provisioning project for user ${user.subject} succeeded.") } yield () } diff --git a/tests/docker/config/delta-postgres.conf b/tests/docker/config/delta-postgres.conf index ef688730f4..1f12bc4058 100644 --- a/tests/docker/config/delta-postgres.conf +++ b/tests/docker/config/delta-postgres.conf @@ -63,6 +63,17 @@ app { propagation-delay = 3 seconds } } + + automatic-provisioning { + enabled = true + permissions = [ + "resources/read", + "resources/write" + ] + enabled-realms { + internal = "internal" + } + } } plugins { diff --git a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/iam/IdentitiesSpec.scala b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/iam/IdentitiesSpec.scala index 051f6c65de..39269dcde1 100644 --- a/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/iam/IdentitiesSpec.scala +++ b/tests/src/test/scala/ch/epfl/bluebrain/nexus/tests/iam/IdentitiesSpec.scala @@ -2,20 +2,42 @@ package ch.epfl.bluebrain.nexus.tests.iam import akka.http.scaladsl.model.StatusCodes import ch.epfl.bluebrain.nexus.tests.HttpClient.tokensMap -import ch.epfl.bluebrain.nexus.tests.{BaseIntegrationSpec, Identity} +import ch.epfl.bluebrain.nexus.tests.Identity.projects.Bojack +import ch.epfl.bluebrain.nexus.tests.iam.types.{Permission, User} +import ch.epfl.bluebrain.nexus.tests.{BaseIntegrationSpec, Identity, Realm} import io.circe.Json class IdentitiesSpec extends BaseIntegrationSpec { + private val internaRealmName = Realm.internal.name + private val provisionedProjectName = Identity.ServiceAccount.name + private val provisionedProject = s"$internaRealmName/$provisionedProjectName" + private val provisionedProjectPermissions = Permission.Resources.list.toSet + "The /identities endpoint" should { - s"return identities of the user" in { - deltaClient.get[Json]("/identities", Identity.ServiceAccount) { (json, response) => - response.status shouldEqual StatusCodes.OK - json shouldEqual jsonContentOf( - "iam/identities/response.json", - "deltaUri" -> config.deltaUri.toString() - ) - } + s"return identities of the user and provision a project for him" in { + for { + _ <- createOrg(Identity.ServiceAccount, internaRealmName) + _ <- deltaClient.get[Json]("/identities", Identity.ServiceAccount) { (json, response) => + response.status shouldEqual StatusCodes.OK + json shouldEqual jsonContentOf( + "iam/identities/response.json", + "deltaUri" -> config.deltaUri.toString() + ) + } + // A project should have been provisioned + _ <- deltaClient.get[Json](s"/projects/$provisionedProject", Identity.ServiceAccount) { (_, response) => + response.status shouldEqual StatusCodes.OK + } + // Checking acls has been properly set + _ <- aclDsl.fetch(s"/$provisionedProject", Identity.ServiceAccount) { acls => + acls._total shouldEqual 1 + val acl = acls._results.headOption.value.acl.head + acl.identity shouldEqual User(internaRealmName, Identity.ServiceAccount.name) + acl.permissions shouldEqual provisionedProjectPermissions + } + } yield succeed + } "return the error for an invalid token" in {