Skip to content

Commit

Permalink
Allow acl provisioning at startup
Browse files Browse the repository at this point in the history
  • Loading branch information
Simon Dumas committed Nov 21, 2024
1 parent 1956c6c commit 8953ace
Show file tree
Hide file tree
Showing 26 changed files with 449 additions and 93 deletions.
5 changes: 5 additions & 0 deletions delta/app/src/main/resources/app.conf
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,11 @@ app {
acls {
# the acls event log configuration
event-log = ${app.defaults.event-log}
# configuration to provision acls at startup
provisioning {
enabled = false
#path = "/path-to-acl"
}
}

# Permissions configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ch.epfl.bluebrain.nexus.delta.provisioning

import cats.syntax.all._
import cats.effect.IO
import ch.epfl.bluebrain.nexus.delta.sdk.ProvisioningAction

trait ProvisioningCoordinator

object ProvisioningCoordinator extends ProvisioningCoordinator {

def apply(actions: Vector[ProvisioningAction]): IO[ProvisioningCoordinator] = {
actions.traverse(_.run).as(ProvisioningCoordinator)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteCon
import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering
import ch.epfl.bluebrain.nexus.delta.routes.{AclsRoutes, UserPermissionsRoutes}
import ch.epfl.bluebrain.nexus.delta.sdk._
import ch.epfl.bluebrain.nexus.delta.sdk.acls.{AclCheck, Acls, AclsImpl}
import ch.epfl.bluebrain.nexus.delta.sdk.acls._
import ch.epfl.bluebrain.nexus.delta.sdk.deletion.ProjectDeletionTask
import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities
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.permissions.{Permissions, StoragePermissionProvider}
import ch.epfl.bluebrain.nexus.delta.sourcing.Transactors
Expand All @@ -26,21 +27,17 @@ object AclsModule extends ModuleDef {

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

make[Acls].from {
(
permissions: Permissions,
config: AppConfig,
xas: Transactors,
clock: Clock[IO]
) =>
acls.AclsImpl(
permissions.fetchPermissionSet,
AclsImpl.findUnknownRealms(xas),
permissions.minimum,
config.acls.eventLog,
xas,
clock
)
make[AclsConfig].from { (config: AppConfig) => config.acls }

make[Acls].from { (permissions: Permissions, config: AclsConfig, xas: Transactors, clock: Clock[IO]) =>
acls.AclsImpl(
permissions.fetchPermissionSet,
AclsImpl.findUnknownRealms(xas),
permissions.minimum,
config.eventLog,
xas,
clock
)
}

make[AclCheck].from { (acls: Acls) => AclCheck(acls) }
Expand All @@ -57,6 +54,10 @@ object AclsModule extends ModuleDef {
new AclsRoutes(identities, acls, aclCheck)(baseUri, cr, ordering)
}

make[AclProvisioning].from { (acls: Acls, config: AclsConfig, serviceAccount: ServiceAccount) =>
new AclProvisioning(acls, config.provisioning, serviceAccount)
}

many[ProjectDeletionTask].add { (acls: Acls) => Acls.projectDeletionTask(acls) }

many[MetadataContextValue].addEffect(MetadataContextValue.fromFile("contexts/acls-metadata.json"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,23 @@ import ch.epfl.bluebrain.nexus.delta.Main.pluginsMaxPriority
import ch.epfl.bluebrain.nexus.delta.config.{AppConfig, StrictEntity}
import ch.epfl.bluebrain.nexus.delta.kernel.dependency.ComponentDescription.PluginDescription
import ch.epfl.bluebrain.nexus.delta.kernel.utils.{ClasspathResourceLoader, IOFuture, UUIDF}
import ch.epfl.bluebrain.nexus.delta.provisioning.ProvisioningCoordinator
import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.contexts
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdJavaApi}
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.ErrorRoutes
import ch.epfl.bluebrain.nexus.delta.sdk.IndexingAction.AggregateIndexingAction
import ch.epfl.bluebrain.nexus.delta.sdk._
import ch.epfl.bluebrain.nexus.delta.sdk.acls.Acls
import ch.epfl.bluebrain.nexus.delta.sdk.acls.{AclProvisioning, Acls}
import ch.epfl.bluebrain.nexus.delta.sdk.fusion.FusionConfig
import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.ServiceAccount
import ch.epfl.bluebrain.nexus.delta.sdk.jws.JWSPayloadHelper
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.{RdfExceptionHandler, RdfRejectionHandler}
import ch.epfl.bluebrain.nexus.delta.sdk.model._
import ch.epfl.bluebrain.nexus.delta.sdk.plugin.PluginDef
import ch.epfl.bluebrain.nexus.delta.sdk.projects.{OwnerPermissionsScopeInitialization, ProjectsConfig, ScopeInitializationErrorStore}
import ch.epfl.bluebrain.nexus.delta.sdk.realms.RealmProvisioning
import ch.epfl.bluebrain.nexus.delta.sourcing.Transactors
import ch.epfl.bluebrain.nexus.delta.sourcing.config._
import ch.megard.akka.http.cors.scaladsl.settings.CorsSettings
Expand Down Expand Up @@ -89,6 +91,10 @@ class DeltaModule(appCfg: AppConfig, config: Config)(implicit classLoader: Class
ScopeInitializer(inits, errorStore)
}

make[ProvisioningCoordinator].fromEffect { (realmProvisioning: RealmProvisioning, aclProvisioning: AclProvisioning) =>
ProvisioningCoordinator(Vector(realmProvisioning, aclProvisioning))
}

make[RemoteContextResolution].named("aggregate").fromEffect { (otherCtxResolutions: Set[RemoteContextResolution]) =>
for {
bulkOpCtx <- ContextValue.fromFile("contexts/bulk-operation.json")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,8 @@ object RealmsModule extends ModuleDef {
RealmsImpl(cfg, wellKnownResolver, xas, clock)
}

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

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

make[RealmsRoutes].from {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package ch.epfl.bluebrain.nexus.delta.kernel.error

import java.nio.file.Path

/**
* Top level error type that represents error loading external files.
*
* @param reason
* the reason of the error
*/
abstract class LoadFileError(reason: String) extends Exception { self =>
override def fillInStackTrace(): Throwable = self
final override def getMessage: String = reason
}

object LoadFileError {

final case class UnaccessibleFile(path: Path, throwable: Throwable)
extends LoadFileError(s"File at path '$path' could not be loaded because of '${throwable.getMessage}'.")

final case class InvalidJson(path: Path, details: String)
extends LoadFileError(s"File at path '$path' does not contain the expected json input: '$details'.")

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
package ch.epfl.bluebrain.nexus.delta.kernel.utils

import cats.effect.IO
import cats.syntax.all._
import ch.epfl.bluebrain.nexus.delta.kernel.error.LoadFileError.{InvalidJson, UnaccessibleFile}
import io.circe.Decoder
import io.circe.parser.decode

import java.nio.file.{Files, Path}
import scala.util.Try

object FileUtils {

/**
Expand All @@ -19,4 +28,24 @@ object FileUtils {
}
}

/**
* Load the content of the given file as a string
*/
def loadAsString(filePath: Path): IO[String] = IO.fromEither(
Try(Files.readString(filePath)).toEither.leftMap(UnaccessibleFile(filePath, _))
)

/**
* Load the content of the given file as json and try to decode it as an A
* @param filePath
* the path of the target file
*/
def loadJsonAs[A: Decoder](filePath: Path): IO[A] =
for {
content <- IO.fromEither(
Try(Files.readString(filePath)).toEither.leftMap(UnaccessibleFile(filePath, _))
)
json <- IO.fromEither(decode[A](content).leftMap { e => InvalidJson(filePath, e.getMessage) })
} yield json

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package ch.epfl.bluebrain.nexus.delta.plugins.search.model

import cats.effect.IO
import cats.syntax.all._
import ch.epfl.bluebrain.nexus.delta.kernel.utils.FileUtils
import ch.epfl.bluebrain.nexus.delta.kernel.utils.FileUtils.loadJsonAs
import ch.epfl.bluebrain.nexus.delta.plugins.compositeviews.model.CompositeView.{Interval, RebuildStrategy}
import ch.epfl.bluebrain.nexus.delta.plugins.compositeviews.model.TemplateSparqlConstructQuery
import ch.epfl.bluebrain.nexus.delta.plugins.search.model.SearchConfig.IndexingConfig
Expand All @@ -15,12 +17,11 @@ import ch.epfl.bluebrain.nexus.delta.sdk.Defaults
import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.HttpResponseFields
import ch.epfl.bluebrain.nexus.delta.sourcing.model.{IriFilter, Label, ProjectRef}
import com.typesafe.config.Config
import io.circe.parser._
import io.circe.syntax.KeyOps
import io.circe.{Decoder, Encoder, JsonObject}
import io.circe.{Encoder, JsonObject}
import pureconfig.{ConfigReader, ConfigSource}

import java.nio.file.{Files, Path}
import java.nio.file.Path
import scala.concurrent.duration.FiniteDuration
import scala.util.Try

Expand Down Expand Up @@ -49,18 +50,19 @@ object SearchConfig {
* Converts a [[Config]] into an [[SearchConfig]]
*/
def load(config: Config): IO[SearchConfig] = {
val pluginConfig = config.getConfig("plugins.search")
val pluginConfig = config.getConfig("plugins.search")
def getFilePath(configPath: String) = Path.of(pluginConfig.getString(configPath))
def loadSuites = {
val suiteSource = ConfigSource.fromConfig(pluginConfig).at("suites")
IO.fromEither(suiteSource.load[Suites].leftMap(InvalidSuites))
}
for {
fields <- loadOption(pluginConfig, "fields", loadExternalConfig[JsonObject])
resourceTypes <- loadExternalConfig[IriFilter](pluginConfig.getString("indexing.resource-types"))
mapping <- loadExternalConfig[JsonObject](pluginConfig.getString("indexing.mapping"))
settings <- loadOption(pluginConfig, "indexing.settings", loadExternalConfig[JsonObject])
query <- loadSparqlQuery(pluginConfig.getString("indexing.query"))
context <- loadOption(pluginConfig, "indexing.context", loadExternalConfig[JsonObject])
fields <- loadOption(pluginConfig, "fields", loadJsonAs[JsonObject])
resourceTypes <- loadJsonAs[IriFilter](getFilePath("indexing.resource-types"))
mapping <- loadJsonAs[JsonObject](getFilePath("indexing.mapping"))
settings <- loadOption(pluginConfig, "indexing.settings", loadJsonAs[JsonObject])
query <- loadSparqlQuery(getFilePath("indexing.query"))
context <- loadOption(pluginConfig, "indexing.context", loadJsonAs[JsonObject])
rebuild <- loadRebuildStrategy(pluginConfig)
defaults <- loadDefaults(pluginConfig)
suites <- loadSuites
Expand All @@ -79,36 +81,21 @@ object SearchConfig {
)
}

private def loadOption[A](config: Config, path: String, io: String => IO[A]) =
private def loadOption[A](config: Config, path: String, io: Path => IO[A]) =
if (config.hasPath(path))
io(config.getString(path)).map(Some(_))
io(Path.of(config.getString(path))).map(Some(_))
else IO.none

private def loadExternalConfig[A: Decoder](filePath: String): IO[A] =
private def loadSparqlQuery(filePath: Path): IO[SparqlConstructQuery] =
for {
content <- IO.fromEither(
Try(Files.readString(Path.of(filePath))).toEither.leftMap(LoadingFileError(filePath, _))
)
json <- IO.fromEither(decode[A](content).leftMap { e => InvalidJsonError(filePath, e.getMessage) })
} yield json

private def loadSparqlQuery(filePath: String): IO[SparqlConstructQuery] =
for {
content <- IO.fromEither(
Try(Files.readString(Path.of(filePath))).toEither.leftMap(LoadingFileError(filePath, _))
)
content <- FileUtils.loadAsString(filePath)
json <- IO.fromEither(TemplateSparqlConstructQuery(content).leftMap { e =>
InvalidSparqlConstructQuery(filePath, e)
})
} yield json

private def loadDefaults(config: Config): IO[Defaults] =
IO.fromEither(
Try(
ConfigSource.fromConfig(config).at("defaults").loadOrThrow[Defaults]
// TODO: Use a correct error
).toEither.leftMap(_ => InvalidJsonError("string", "string"))
)
IO.pure(ConfigSource.fromConfig(config).at("defaults").loadOrThrow[Defaults])

/**
* Load the rebuild strategy from the search config. If either of the required fields is null, missing, or not a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,13 @@ package ch.epfl.bluebrain.nexus.delta.plugins.search.model
import ch.epfl.bluebrain.nexus.delta.sdk.error.SDKError
import pureconfig.error.ConfigReaderFailures

import java.nio.file.Path
import scala.concurrent.duration.FiniteDuration

abstract class SearchConfigError(val reason: String) extends SDKError

object SearchConfigError {

final case class LoadingFileError(path: String, throwable: Throwable)
extends SearchConfigError(s"File at path '$path' could not be loaded because of '${throwable.getMessage}'.")

final case class InvalidJsonError(path: String, details: String)
extends SearchConfigError(s"File at path '$path' does not contain a the expect json: '$details'.")

final case class InvalidSparqlConstructQuery(path: String, details: String)
final case class InvalidSparqlConstructQuery(path: Path, details: String)
extends SearchConfigError(s"File at path '$path' does not contain a valid SPARQL construct query: '$details'.")

final case class InvalidSuites(failures: ConfigReaderFailures)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package ch.epfl.bluebrain.nexus.delta.plugins.search.model

import ch.epfl.bluebrain.nexus.delta.kernel.error.LoadFileError.{InvalidJson, UnaccessibleFile}
import ch.epfl.bluebrain.nexus.delta.plugins.compositeviews.model.CompositeView.Interval
import ch.epfl.bluebrain.nexus.delta.plugins.search.model.SearchConfigError.{InvalidJsonError, InvalidSparqlConstructQuery, LoadingFileError}
import ch.epfl.bluebrain.nexus.delta.plugins.search.model.SearchConfigError.InvalidSparqlConstructQuery
import ch.epfl.bluebrain.nexus.delta.sdk.Defaults
import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef}
import ch.epfl.bluebrain.nexus.testkit.scalatest.ce.CatsEffectSpec
Expand Down Expand Up @@ -79,7 +80,7 @@ class SearchConfigSpec extends CatsEffectSpec {
val missingContextFile = config(validJson, validJson, Some(validJson), validQuery, Some(missingFile))
val all = List(missingFieldFile, missingEsMapping, missingEsSettings, missingSparqlFile, missingContextFile)
forAll(all) { c =>
SearchConfig.load(c).rejectedWith[LoadingFileError]
SearchConfig.load(c).rejectedWith[UnaccessibleFile]
}
}

Expand All @@ -90,7 +91,7 @@ class SearchConfigSpec extends CatsEffectSpec {
val invalidContext = config(validJson, validJson, Some(validJson), validQuery, Some(emptyFile))
val all = List(invalidFields, invalidEsMapping, invalidEsSettings, invalidContext)
forAll(all) { c =>
SearchConfig.load(c).rejectedWith[InvalidJsonError]
SearchConfig.load(c).rejectedWith[InvalidJson]
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ch.epfl.bluebrain.nexus.delta.sdk

import cats.effect.IO
import ch.epfl.bluebrain.nexus.delta.sdk.ProvisioningAction.Outcome

/**
* Provisioning action to run at startup
*/
trait ProvisioningAction {

def run: IO[Outcome]

}

object ProvisioningAction {

sealed trait Outcome

object Outcome {
case object Success extends Outcome
case object Skipped extends Outcome
case object Disabled extends Outcome
case object Error extends Outcome
}

}
Loading

0 comments on commit 8953ace

Please sign in to comment.