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

Allow to provision realms at startup #5220

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions delta/app/src/main/resources/app.conf
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,20 @@ app {
event-log = ${app.defaults.event-log}
# the realms pagination config
pagination = ${app.defaults.pagination}

# To provision realms at startup
# Only the name and the OpenId config url are mandatory
provisioning {
enabled = false
realms {
# my-realm = {
# name = "My realm name"
# open-id-config = "https://.../path/.well-known/openid-configuration"
# logo = "https://bbp.epfl.ch/path/favicon.png"
# accepted-audiences = ["audience1", "audience2"]
#}
}
}
}

# Organizations configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,27 @@
package ch.epfl.bluebrain.nexus.delta.routes

import akka.http.scaladsl.model.{StatusCode, StatusCodes, Uri}
import akka.http.scaladsl.model.{StatusCode, StatusCodes}
import akka.http.scaladsl.server.{Directive1, Route}
import cats.data.NonEmptySet
import cats.effect.IO
import cats.implicits._
import ch.epfl.bluebrain.nexus.delta.kernel.circe.CirceUnmarshalling
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder
import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering
import ch.epfl.bluebrain.nexus.delta.routes.RealmsRoutes.RealmInput
import ch.epfl.bluebrain.nexus.delta.routes.RealmsRoutes.RealmInput._
import ch.epfl.bluebrain.nexus.delta.sdk.RealmResource
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.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.implicits._
import ch.epfl.bluebrain.nexus.delta.sdk.model.BaseUri
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchParams.RealmSearchParams
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults._
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.{PaginationConfig, SearchResults}
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, Name}
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.{realms => realmsPermissions}
import ch.epfl.bluebrain.nexus.delta.sdk.realms.Realms
import ch.epfl.bluebrain.nexus.delta.sdk.realms.model.{Realm, RealmRejection}
import io.circe.Decoder
import io.circe.generic.extras.Configuration
import io.circe.generic.extras.semiauto.deriveConfiguredDecoder
import ch.epfl.bluebrain.nexus.delta.sdk.realms.model.{Realm, RealmFields, RealmRejection}

class RealmsRoutes(identities: Identities, realms: Realms, aclCheck: AclCheck)(implicit
baseUri: BaseUri,
Expand Down Expand Up @@ -74,16 +68,11 @@ class RealmsRoutes(identities: Identities, realms: Realms, aclCheck: AclCheck)(i
parameter("rev".as[Int].?) {
case Some(rev) =>
// Update a realm
entity(as[RealmInput]) { case RealmInput(name, openIdConfig, logo, acceptedAudiences) =>
emitMetadata(realms.update(id, rev, name, openIdConfig, logo, acceptedAudiences))
}
entity(as[RealmFields]) { fields => emitMetadata(realms.update(id, rev, fields)) }
case None =>
// Create a realm
entity(as[RealmInput]) { case RealmInput(name, openIdConfig, logo, acceptedAudiences) =>
emitMetadata(
StatusCodes.Created,
realms.create(id, name, openIdConfig, logo, acceptedAudiences)
)
entity(as[RealmFields]) { fields =>
emitMetadata(StatusCodes.Created, realms.create(id, fields))
}
}
}
Expand Down Expand Up @@ -117,18 +106,6 @@ class RealmsRoutes(identities: Identities, realms: Realms, aclCheck: AclCheck)(i

object RealmsRoutes {

implicit final private val configuration: Configuration = Configuration.default.withStrictDecoding

final private[routes] case class RealmInput(
name: Name,
openIdConfig: Uri,
logo: Option[Uri],
acceptedAudiences: Option[NonEmptySet[String]]
)
private[routes] object RealmInput {
implicit val realmDecoder: Decoder[RealmInput] = deriveConfiguredDecoder[RealmInput]
}

/**
* @return
* the [[Route]] for realms
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import ch.epfl.bluebrain.nexus.delta.routes.RealmsRoutes
import ch.epfl.bluebrain.nexus.delta.sdk._
import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck
import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities
import ch.epfl.bluebrain.nexus.delta.sdk.model.MetadataContextValue
import ch.epfl.bluebrain.nexus.delta.sdk.realms.{Realms, RealmsImpl}
import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.ServiceAccount
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, MetadataContextValue}
import ch.epfl.bluebrain.nexus.delta.sdk.realms.{RealmProvisioning, Realms, RealmsConfig, RealmsImpl}
import ch.epfl.bluebrain.nexus.delta.sourcing.Transactors
import izumi.distage.model.definition.{Id, ModuleDef}

Expand All @@ -27,27 +28,35 @@ object RealmsModule extends ModuleDef {

implicit private val loader: ClasspathResourceLoader = ClasspathResourceLoader.withContext(getClass)

make[RealmsConfig].from { (cfg: AppConfig) => cfg.realms }

make[Realms].from {
(
cfg: AppConfig,
cfg: RealmsConfig,
clock: Clock[IO],
hc: HttpClient @Id("realm"),
xas: Transactors
) =>
val wellKnownResolver = realms.WellKnownResolver((uri: Uri) => hc.toJson(HttpRequest(uri = uri))) _
RealmsImpl(cfg.realms, wellKnownResolver, xas, clock)
RealmsImpl(cfg, wellKnownResolver, xas, clock)
}

make[RealmProvisioning].fromEffect { (realms: Realms, cfg: RealmsConfig, serviceAccount: ServiceAccount) =>
RealmProvisioning(realms, cfg.provisioning, serviceAccount)

}

make[RealmsRoutes].from {
(
identities: Identities,
realms: Realms,
cfg: AppConfig,
cfg: RealmsConfig,
aclCheck: AclCheck,
baseUri: BaseUri,
cr: RemoteContextResolution @Id("aggregate"),
ordering: JsonKeyOrdering
) =>
new RealmsRoutes(identities, realms, aclCheck)(cfg.http.baseUri, cfg.realms.pagination, cr, ordering)
new RealmsRoutes(identities, realms, aclCheck)(baseUri, cfg.pagination, cr, ordering)
}

make[HttpClient].named("realm").from { (as: ActorSystem) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.implicits._
import ch.epfl.bluebrain.nexus.delta.sdk.model.Name
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.{realms => realmsPermissions}
import ch.epfl.bluebrain.nexus.delta.sdk.realms.model.RealmRejection.UnsuccessfulOpenIdConfigResponse
import ch.epfl.bluebrain.nexus.delta.sdk.realms.{RealmsConfig, RealmsImpl}
import ch.epfl.bluebrain.nexus.delta.sdk.realms.{RealmsConfig, RealmsImpl, RealmsProvisioningConfig}
import ch.epfl.bluebrain.nexus.delta.sdk.utils.BaseRouteSpec
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group, Subject}
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label
Expand All @@ -28,7 +28,8 @@ class RealmsRoutesSpec extends BaseRouteSpec with IOFromMap {

val githubLogo: Uri = "https://localhost/ghlogo"

val config: RealmsConfig = RealmsConfig(eventLogConfig, pagination)
private val provisioning = RealmsProvisioningConfig(enabled = false, Map.empty)
private val config = RealmsConfig(eventLogConfig, pagination, provisioning)

val (githubOpenId, githubWk) = WellKnownGen.create(github.value)
val (gitlabOpenId, gitlabWk) = WellKnownGen.create(gitlab.value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ import com.typesafe.config.Config
import io.circe.parser._
import io.circe.syntax.KeyOps
import io.circe.{Decoder, Encoder, JsonObject}
import pureconfig.configurable.genericMapReader
import pureconfig.error.CannotConvert
import pureconfig.{ConfigReader, ConfigSource}

import java.nio.file.{Files, Path}
Expand All @@ -39,8 +37,7 @@ object SearchConfig {
type Suites = Map[Label, Suite]

case class NamedSuite(name: Label, suite: Suite)
implicit private val suitesMapReader: ConfigReader[Suites] =
genericMapReader(str => Label(str).leftMap(e => CannotConvert(str, classOf[Label].getSimpleName, e.getMessage)))
implicit private val suitesMapReader: ConfigReader[Suites] = Label.labelMapReader[Suite]

implicit val suiteEncoder: Encoder[NamedSuite] =
Encoder[JsonObject].contramap(s => JsonObject("projects" := s.suite, "name" := s.name))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package ch.epfl.bluebrain.nexus.delta.sdk.model

import cats.implicits._
import ch.epfl.bluebrain.nexus.delta.kernel.error.FormatError
import ch.epfl.bluebrain.nexus.delta.sdk.error.FormatErrors.IllegalNameFormatError
import io.circe.{Decoder, Encoder}
import pureconfig.ConfigReader
import pureconfig.error.CannotConvert

import scala.util.matching.Regex
import cats.implicits._

/**
* A valid name value that can be used to describe resources, like for example the display name of a realm.
Expand Down Expand Up @@ -48,4 +50,9 @@ object Name {
implicit final val nameDecoder: Decoder[Name] =
Decoder.decodeString.emap(str => Name(str).leftMap(_.getMessage))

implicit final val nameConfigReader: ConfigReader[Name] =
ConfigReader.fromString(str =>
Name(str).leftMap(err => CannotConvert(str, classOf[Name].getSimpleName, err.getMessage))
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission
import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.{ApiMappings, PrefixIri, ProjectFields}
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label
import pureconfig.ConfigReader
import pureconfig.configurable._
import pureconfig.error.CannotConvert

/**
Expand Down Expand Up @@ -43,8 +42,7 @@ object AutomaticProvisioningConfig {
Permission(str).leftMap(err => CannotConvert(str, classOf[Permission].getSimpleName, err.getMessage))
)

implicit private val mapReader: ConfigReader[Map[Label, Label]] =
genericMapReader(str => Label(str).leftMap(e => CannotConvert(str, classOf[Label].getSimpleName, e.getMessage)))
implicit private val mapReader: ConfigReader[Map[Label, Label]] = Label.labelMapReader[Label]

implicit private val prefixIriReader: ConfigReader[PrefixIri] = ConfigReader[Iri].emap { iri =>
PrefixIri(iri).leftMap { e => CannotConvert(iri.toString, classOf[PrefixIri].getSimpleName, e.getMessage) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package ch.epfl.bluebrain.nexus.delta.sdk.realms

import cats.effect.IO
import cats.syntax.all._
import ch.epfl.bluebrain.nexus.delta.kernel.Logger
import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.ServiceAccount
import ch.epfl.bluebrain.nexus.delta.sdk.realms.model.RealmRejection.RealmAlreadyExists
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject

/**
* Provision the different realms provided in the configuration
*/
trait RealmProvisioning

object RealmProvisioning extends RealmProvisioning {

private val logger = Logger[RealmProvisioning]

def apply(
realms: Realms,
config: RealmsProvisioningConfig,
serviceAccount: ServiceAccount
): IO[RealmProvisioning.type] =
if (config.enabled) {
implicit val serviceAccountSubject: Subject = serviceAccount.subject
for {
_ <- logger.info(s"Realm provisioning is active. Creating ${config.realms.size} realms...")
_ <- config.realms.toList.traverse { case (label, fields) =>
realms.create(label, fields).recoverWith {
case r: RealmAlreadyExists => logger.debug(r)(s"Realm '$label' already exists")
case e => logger.error(e)(s"Realm '$label' could not be created: '${e.getMessage}'")
}
}
_ <- logger.info(s"Provisioning ${config.realms.size} realms is completed")
} yield RealmProvisioning
} else logger.info(s"Realm provisioning is inactive.").as(RealmProvisioning)

}
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package ch.epfl.bluebrain.nexus.delta.sdk.realms

import akka.http.scaladsl.model.Uri
import cats.data.NonEmptySet
import cats.effect.{Clock, IO}

import cats.implicits._
import ch.epfl.bluebrain.nexus.delta.kernel.search.Pagination.FromPagination
import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri
import ch.epfl.bluebrain.nexus.delta.sdk.RealmResource
import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceUris
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchParams.RealmSearchParams
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults.UnscoredSearchResults
import ch.epfl.bluebrain.nexus.delta.sdk.model.{Name, ResourceUris}
import ch.epfl.bluebrain.nexus.delta.sdk.realms.model.RealmCommand.{CreateRealm, DeprecateRealm, UpdateRealm}
import ch.epfl.bluebrain.nexus.delta.sdk.realms.model.RealmEvent.{RealmCreated, RealmDeprecated, RealmUpdated}
import ch.epfl.bluebrain.nexus.delta.sdk.realms.model.RealmRejection.{IncorrectRev, RealmAlreadyDeprecated, RealmAlreadyExists, RealmNotFound}
Expand All @@ -18,7 +17,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.syntax._
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject
import ch.epfl.bluebrain.nexus.delta.sourcing.model.{EntityType, Label}
import ch.epfl.bluebrain.nexus.delta.sourcing.{GlobalEntityDefinition, StateMachine}
import cats.implicits._

/**
* Operations pertaining to managing realms.
Expand All @@ -30,21 +28,12 @@ trait Realms {
*
* @param label
* the realm label
* @param name
* the name of the realm
* @param openIdConfig
* the address of the openid configuration
* @param logo
* an optional realm logo
* @param acceptedAudiences
* the optional set of audiences of this realm. JWT with `aud` which do not match this field will be rejected
* @param fields
* the realm information
*/
def create(
label: Label,
name: Name,
openIdConfig: Uri,
logo: Option[Uri],
acceptedAudiences: Option[NonEmptySet[String]]
fields: RealmFields
)(implicit caller: Subject): IO[RealmResource]

/**
Expand All @@ -54,22 +43,13 @@ trait Realms {
* the realm label
* @param rev
* the current revision of the realm
* @param name
* the new name for the realm
* @param openIdConfig
* the new openid configuration address
* @param logo
* an optional new logo
* @param acceptedAudiences
* the optional set of audiences of this realm. JWT with `aud` which do not match this field will be rejected
* @param fields
* the realm information
*/
def update(
label: Label,
rev: Int,
name: Name,
openIdConfig: Uri,
logo: Option[Uri],
acceptedAudiences: Option[NonEmptySet[String]]
fields: RealmFields
)(implicit caller: Subject): IO[RealmResource]

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ import pureconfig.generic.semiauto.deriveReader
* The event log configuration
* @param pagination
* configuration for how pagination should behave in listing operations
* @param provisioning
* configuration to provision realms at startup
*/
final case class RealmsConfig(
eventLog: EventLogConfig,
pagination: PaginationConfig
pagination: PaginationConfig,
provisioning: RealmsProvisioningConfig
)

object RealmsConfig {
Expand Down
Loading
Loading