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

Init acls provisioning #5222

Merged
merged 8 commits into from
Nov 21, 2024
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
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 @@ -31,8 +31,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceF._
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults.searchResultsJsonLdEncoder
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.{acls => aclsPermissions}
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission
import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Identity, Label, ProjectRef}
import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef}
import io.circe._
import io.circe.generic.extras.Configuration
import io.circe.generic.extras.semiauto.deriveConfiguredDecoder
Expand Down Expand Up @@ -196,23 +195,6 @@ class AclsRoutes(identities: Identities, acls: Acls, aclCheck: AclCheck)(implici

object AclsRoutes {

final private case class IdentityPermissions(identity: Identity, permissions: Set[Permission])

final private[routes] case class AclValues(value: Seq[(Identity, Set[Permission])])

private[routes] object AclValues {

implicit private val identityPermsDecoder: Decoder[IdentityPermissions] = {
implicit val config: Configuration = Configuration.default.withStrictDecoding
deriveConfiguredDecoder[IdentityPermissions]
}

implicit val aclValuesDecoder: Decoder[AclValues] =
Decoder
.decodeSeq[IdentityPermissions]
.map(seq => AclValues(seq.map(value => value.identity -> value.permissions)))
}

final private[routes] case class ReplaceAcl(acl: AclValues)
private[routes] object ReplaceAcl {

Expand Down
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
@@ -1,7 +1,8 @@
package ch.epfl.bluebrain.nexus.delta.routes

import ch.epfl.bluebrain.nexus.delta.routes.AclsRoutes.PatchAcl.{Append, Subtract}
import ch.epfl.bluebrain.nexus.delta.routes.AclsRoutes.{AclValues, PatchAcl, ReplaceAcl}
import ch.epfl.bluebrain.nexus.delta.routes.AclsRoutes.{PatchAcl, ReplaceAcl}
import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.AclValues
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.resources
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity
import ch.epfl.bluebrain.nexus.testkit.scalatest.BaseSpec
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
Loading