Skip to content

Commit e5993bf

Browse files
imsduSimon Dumas
andauthored
Allow to provision realms at startup (#5220)
* Allow to provision realms at startup --------- Co-authored-by: Simon Dumas <simon.dumas@epfl.ch>
1 parent 4448ec7 commit e5993bf

File tree

17 files changed

+313
-94
lines changed

17 files changed

+313
-94
lines changed

delta/app/src/main/resources/app.conf

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,20 @@ app {
225225
event-log = ${app.defaults.event-log}
226226
# the realms pagination config
227227
pagination = ${app.defaults.pagination}
228+
229+
# To provision realms at startup
230+
# Only the name and the OpenId config url are mandatory
231+
provisioning {
232+
enabled = false
233+
realms {
234+
# my-realm = {
235+
# name = "My realm name"
236+
# open-id-config = "https://.../path/.well-known/openid-configuration"
237+
# logo = "https://bbp.epfl.ch/path/favicon.png"
238+
# accepted-audiences = ["audience1", "audience2"]
239+
#}
240+
}
241+
}
228242
}
229243

230244
# Organizations configuration

delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/routes/RealmsRoutes.scala

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,27 @@
11
package ch.epfl.bluebrain.nexus.delta.routes
22

3-
import akka.http.scaladsl.model.{StatusCode, StatusCodes, Uri}
3+
import akka.http.scaladsl.model.{StatusCode, StatusCodes}
44
import akka.http.scaladsl.server.{Directive1, Route}
5-
import cats.data.NonEmptySet
65
import cats.effect.IO
76
import cats.implicits._
87
import ch.epfl.bluebrain.nexus.delta.kernel.circe.CirceUnmarshalling
98
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution
109
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder
1110
import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering
12-
import ch.epfl.bluebrain.nexus.delta.routes.RealmsRoutes.RealmInput
13-
import ch.epfl.bluebrain.nexus.delta.routes.RealmsRoutes.RealmInput._
1411
import ch.epfl.bluebrain.nexus.delta.sdk.RealmResource
1512
import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck
1613
import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.AclAddress
1714
import ch.epfl.bluebrain.nexus.delta.sdk.directives.AuthDirectives
1815
import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._
1916
import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities
2017
import ch.epfl.bluebrain.nexus.delta.sdk.implicits._
18+
import ch.epfl.bluebrain.nexus.delta.sdk.model.BaseUri
2119
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchParams.RealmSearchParams
2220
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults._
2321
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.{PaginationConfig, SearchResults}
24-
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, Name}
2522
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.{realms => realmsPermissions}
2623
import ch.epfl.bluebrain.nexus.delta.sdk.realms.Realms
27-
import ch.epfl.bluebrain.nexus.delta.sdk.realms.model.{Realm, RealmRejection}
28-
import io.circe.Decoder
29-
import io.circe.generic.extras.Configuration
30-
import io.circe.generic.extras.semiauto.deriveConfiguredDecoder
24+
import ch.epfl.bluebrain.nexus.delta.sdk.realms.model.{Realm, RealmFields, RealmRejection}
3125

3226
class RealmsRoutes(identities: Identities, realms: Realms, aclCheck: AclCheck)(implicit
3327
baseUri: BaseUri,
@@ -74,16 +68,11 @@ class RealmsRoutes(identities: Identities, realms: Realms, aclCheck: AclCheck)(i
7468
parameter("rev".as[Int].?) {
7569
case Some(rev) =>
7670
// Update a realm
77-
entity(as[RealmInput]) { case RealmInput(name, openIdConfig, logo, acceptedAudiences) =>
78-
emitMetadata(realms.update(id, rev, name, openIdConfig, logo, acceptedAudiences))
79-
}
71+
entity(as[RealmFields]) { fields => emitMetadata(realms.update(id, rev, fields)) }
8072
case None =>
8173
// Create a realm
82-
entity(as[RealmInput]) { case RealmInput(name, openIdConfig, logo, acceptedAudiences) =>
83-
emitMetadata(
84-
StatusCodes.Created,
85-
realms.create(id, name, openIdConfig, logo, acceptedAudiences)
86-
)
74+
entity(as[RealmFields]) { fields =>
75+
emitMetadata(StatusCodes.Created, realms.create(id, fields))
8776
}
8877
}
8978
}
@@ -117,18 +106,6 @@ class RealmsRoutes(identities: Identities, realms: Realms, aclCheck: AclCheck)(i
117106

118107
object RealmsRoutes {
119108

120-
implicit final private val configuration: Configuration = Configuration.default.withStrictDecoding
121-
122-
final private[routes] case class RealmInput(
123-
name: Name,
124-
openIdConfig: Uri,
125-
logo: Option[Uri],
126-
acceptedAudiences: Option[NonEmptySet[String]]
127-
)
128-
private[routes] object RealmInput {
129-
implicit val realmDecoder: Decoder[RealmInput] = deriveConfiguredDecoder[RealmInput]
130-
}
131-
132109
/**
133110
* @return
134111
* the [[Route]] for realms

delta/app/src/main/scala/ch/epfl/bluebrain/nexus/delta/wiring/RealmsModule.scala

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ import ch.epfl.bluebrain.nexus.delta.routes.RealmsRoutes
1414
import ch.epfl.bluebrain.nexus.delta.sdk._
1515
import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck
1616
import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities
17-
import ch.epfl.bluebrain.nexus.delta.sdk.model.MetadataContextValue
18-
import ch.epfl.bluebrain.nexus.delta.sdk.realms.{Realms, RealmsImpl}
17+
import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.ServiceAccount
18+
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, MetadataContextValue}
19+
import ch.epfl.bluebrain.nexus.delta.sdk.realms.{RealmProvisioning, Realms, RealmsConfig, RealmsImpl}
1920
import ch.epfl.bluebrain.nexus.delta.sourcing.Transactors
2021
import izumi.distage.model.definition.{Id, ModuleDef}
2122

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

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

31+
make[RealmsConfig].from { (cfg: AppConfig) => cfg.realms }
32+
3033
make[Realms].from {
3134
(
32-
cfg: AppConfig,
35+
cfg: RealmsConfig,
3336
clock: Clock[IO],
3437
hc: HttpClient @Id("realm"),
3538
xas: Transactors
3639
) =>
3740
val wellKnownResolver = realms.WellKnownResolver((uri: Uri) => hc.toJson(HttpRequest(uri = uri))) _
38-
RealmsImpl(cfg.realms, wellKnownResolver, xas, clock)
41+
RealmsImpl(cfg, wellKnownResolver, xas, clock)
42+
}
43+
44+
make[RealmProvisioning].fromEffect { (realms: Realms, cfg: RealmsConfig, serviceAccount: ServiceAccount) =>
45+
RealmProvisioning(realms, cfg.provisioning, serviceAccount)
46+
3947
}
4048

4149
make[RealmsRoutes].from {
4250
(
4351
identities: Identities,
4452
realms: Realms,
45-
cfg: AppConfig,
53+
cfg: RealmsConfig,
4654
aclCheck: AclCheck,
55+
baseUri: BaseUri,
4756
cr: RemoteContextResolution @Id("aggregate"),
4857
ordering: JsonKeyOrdering
4958
) =>
50-
new RealmsRoutes(identities, realms, aclCheck)(cfg.http.baseUri, cfg.realms.pagination, cr, ordering)
59+
new RealmsRoutes(identities, realms, aclCheck)(baseUri, cfg.pagination, cr, ordering)
5160
}
5261

5362
make[HttpClient].named("realm").from { (as: ActorSystem) =>

delta/app/src/test/scala/ch/epfl/bluebrain/nexus/delta/routes/RealmsRoutesSpec.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.implicits._
1414
import ch.epfl.bluebrain.nexus.delta.sdk.model.Name
1515
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.{realms => realmsPermissions}
1616
import ch.epfl.bluebrain.nexus.delta.sdk.realms.model.RealmRejection.UnsuccessfulOpenIdConfigResponse
17-
import ch.epfl.bluebrain.nexus.delta.sdk.realms.{RealmsConfig, RealmsImpl}
17+
import ch.epfl.bluebrain.nexus.delta.sdk.realms.{RealmsConfig, RealmsImpl, RealmsProvisioningConfig}
1818
import ch.epfl.bluebrain.nexus.delta.sdk.utils.BaseRouteSpec
1919
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group, Subject}
2020
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label
@@ -28,7 +28,8 @@ class RealmsRoutesSpec extends BaseRouteSpec with IOFromMap {
2828

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

31-
val config: RealmsConfig = RealmsConfig(eventLogConfig, pagination)
31+
private val provisioning = RealmsProvisioningConfig(enabled = false, Map.empty)
32+
private val config = RealmsConfig(eventLogConfig, pagination, provisioning)
3233

3334
val (githubOpenId, githubWk) = WellKnownGen.create(github.value)
3435
val (gitlabOpenId, gitlabWk) = WellKnownGen.create(gitlab.value)

delta/plugins/search/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/search/model/SearchConfig.scala

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ import com.typesafe.config.Config
1818
import io.circe.parser._
1919
import io.circe.syntax.KeyOps
2020
import io.circe.{Decoder, Encoder, JsonObject}
21-
import pureconfig.configurable.genericMapReader
22-
import pureconfig.error.CannotConvert
2321
import pureconfig.{ConfigReader, ConfigSource}
2422

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

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

4542
implicit val suiteEncoder: Encoder[NamedSuite] =
4643
Encoder[JsonObject].contramap(s => JsonObject("projects" := s.suite, "name" := s.name))

delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/model/Name.scala

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package ch.epfl.bluebrain.nexus.delta.sdk.model
22

3+
import cats.implicits._
34
import ch.epfl.bluebrain.nexus.delta.kernel.error.FormatError
45
import ch.epfl.bluebrain.nexus.delta.sdk.error.FormatErrors.IllegalNameFormatError
56
import io.circe.{Decoder, Encoder}
7+
import pureconfig.ConfigReader
8+
import pureconfig.error.CannotConvert
69

710
import scala.util.matching.Regex
8-
import cats.implicits._
911

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

53+
implicit final val nameConfigReader: ConfigReader[Name] =
54+
ConfigReader.fromString(str =>
55+
Name(str).leftMap(err => CannotConvert(str, classOf[Name].getSimpleName, err.getMessage))
56+
)
57+
5158
}

delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/provisioning/AutomaticProvisioningConfig.scala

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission
66
import ch.epfl.bluebrain.nexus.delta.sdk.projects.model.{ApiMappings, PrefixIri, ProjectFields}
77
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label
88
import pureconfig.ConfigReader
9-
import pureconfig.configurable._
109
import pureconfig.error.CannotConvert
1110

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

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

4947
implicit private val prefixIriReader: ConfigReader[PrefixIri] = ConfigReader[Iri].emap { iri =>
5048
PrefixIri(iri).leftMap { e => CannotConvert(iri.toString, classOf[PrefixIri].getSimpleName, e.getMessage) }
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package ch.epfl.bluebrain.nexus.delta.sdk.realms
2+
3+
import cats.effect.IO
4+
import cats.syntax.all._
5+
import ch.epfl.bluebrain.nexus.delta.kernel.Logger
6+
import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.ServiceAccount
7+
import ch.epfl.bluebrain.nexus.delta.sdk.realms.model.RealmRejection.RealmAlreadyExists
8+
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject
9+
10+
/**
11+
* Provision the different realms provided in the configuration
12+
*/
13+
trait RealmProvisioning
14+
15+
object RealmProvisioning extends RealmProvisioning {
16+
17+
private val logger = Logger[RealmProvisioning]
18+
19+
def apply(
20+
realms: Realms,
21+
config: RealmsProvisioningConfig,
22+
serviceAccount: ServiceAccount
23+
): IO[RealmProvisioning.type] =
24+
if (config.enabled) {
25+
implicit val serviceAccountSubject: Subject = serviceAccount.subject
26+
for {
27+
_ <- logger.info(s"Realm provisioning is active. Creating ${config.realms.size} realms...")
28+
_ <- config.realms.toList.traverse { case (label, fields) =>
29+
realms.create(label, fields).recoverWith {
30+
case r: RealmAlreadyExists => logger.debug(r)(s"Realm '$label' already exists")
31+
case e => logger.error(e)(s"Realm '$label' could not be created: '${e.getMessage}'")
32+
}
33+
}
34+
_ <- logger.info(s"Provisioning ${config.realms.size} realms is completed")
35+
} yield RealmProvisioning
36+
} else logger.info(s"Realm provisioning is inactive.").as(RealmProvisioning)
37+
38+
}

delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/realms/Realms.scala

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
package ch.epfl.bluebrain.nexus.delta.sdk.realms
22

33
import akka.http.scaladsl.model.Uri
4-
import cats.data.NonEmptySet
54
import cats.effect.{Clock, IO}
6-
5+
import cats.implicits._
76
import ch.epfl.bluebrain.nexus.delta.kernel.search.Pagination.FromPagination
87
import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri
98
import ch.epfl.bluebrain.nexus.delta.sdk.RealmResource
9+
import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceUris
1010
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchParams.RealmSearchParams
1111
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults.UnscoredSearchResults
12-
import ch.epfl.bluebrain.nexus.delta.sdk.model.{Name, ResourceUris}
1312
import ch.epfl.bluebrain.nexus.delta.sdk.realms.model.RealmCommand.{CreateRealm, DeprecateRealm, UpdateRealm}
1413
import ch.epfl.bluebrain.nexus.delta.sdk.realms.model.RealmEvent.{RealmCreated, RealmDeprecated, RealmUpdated}
1514
import ch.epfl.bluebrain.nexus.delta.sdk.realms.model.RealmRejection.{IncorrectRev, RealmAlreadyDeprecated, RealmAlreadyExists, RealmNotFound}
@@ -18,7 +17,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.syntax._
1817
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject
1918
import ch.epfl.bluebrain.nexus.delta.sourcing.model.{EntityType, Label}
2019
import ch.epfl.bluebrain.nexus.delta.sourcing.{GlobalEntityDefinition, StateMachine}
21-
import cats.implicits._
2220

2321
/**
2422
* Operations pertaining to managing realms.
@@ -30,21 +28,12 @@ trait Realms {
3028
*
3129
* @param label
3230
* the realm label
33-
* @param name
34-
* the name of the realm
35-
* @param openIdConfig
36-
* the address of the openid configuration
37-
* @param logo
38-
* an optional realm logo
39-
* @param acceptedAudiences
40-
* the optional set of audiences of this realm. JWT with `aud` which do not match this field will be rejected
31+
* @param fields
32+
* the realm information
4133
*/
4234
def create(
4335
label: Label,
44-
name: Name,
45-
openIdConfig: Uri,
46-
logo: Option[Uri],
47-
acceptedAudiences: Option[NonEmptySet[String]]
36+
fields: RealmFields
4837
)(implicit caller: Subject): IO[RealmResource]
4938

5039
/**
@@ -54,22 +43,13 @@ trait Realms {
5443
* the realm label
5544
* @param rev
5645
* the current revision of the realm
57-
* @param name
58-
* the new name for the realm
59-
* @param openIdConfig
60-
* the new openid configuration address
61-
* @param logo
62-
* an optional new logo
63-
* @param acceptedAudiences
64-
* the optional set of audiences of this realm. JWT with `aud` which do not match this field will be rejected
46+
* @param fields
47+
* the realm information
6548
*/
6649
def update(
6750
label: Label,
6851
rev: Int,
69-
name: Name,
70-
openIdConfig: Uri,
71-
logo: Option[Uri],
72-
acceptedAudiences: Option[NonEmptySet[String]]
52+
fields: RealmFields
7353
)(implicit caller: Subject): IO[RealmResource]
7454

7555
/**

delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/realms/RealmsConfig.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ import pureconfig.generic.semiauto.deriveReader
1212
* The event log configuration
1313
* @param pagination
1414
* configuration for how pagination should behave in listing operations
15+
* @param provisioning
16+
* configuration to provision realms at startup
1517
*/
1618
final case class RealmsConfig(
1719
eventLog: EventLogConfig,
18-
pagination: PaginationConfig
20+
pagination: PaginationConfig,
21+
provisioning: RealmsProvisioningConfig
1922
)
2023

2124
object RealmsConfig {

0 commit comments

Comments
 (0)