Skip to content

Commit

Permalink
Make it easier to configure and to disable JWS for delegation (#5065)
Browse files Browse the repository at this point in the history
* Make it easier to configure and to disable JWS for delegation

* Fix test compilation

---------

Co-authored-by: Simon Dumas <[email protected]>
  • Loading branch information
imsdu and Simon Dumas authored Jul 12, 2024
1 parent c233bab commit 6a9272b
Show file tree
Hide file tree
Showing 18 changed files with 237 additions and 150 deletions.
9 changes: 9 additions & 0 deletions delta/app/src/main/resources/app.conf
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ app {
error-handling: "strict"
}

jws {
# JWS configuration to sign and verify JWS payloads.
# Those are used for delegation operations
type = "disabled"
#type: enabled
#private-key = TO_OVERWRITE
ttl = 3 hours
}

# Identities configuration
identities {
# The max number of tokens in the groups cache
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ch.epfl.bluebrain.nexus.delta.kernel.config.Configs
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.JsonLdApiConfig
import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclsConfig
import ch.epfl.bluebrain.nexus.delta.sdk.fusion.FusionConfig
import ch.epfl.bluebrain.nexus.delta.sdk.jws.JWSConfig
import ch.epfl.bluebrain.nexus.delta.sdk.model.ServiceAccountConfig
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.OrganizationsConfig
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.PermissionsConfig
Expand Down Expand Up @@ -52,7 +53,8 @@ final case class AppConfig(
sse: SseConfig,
projections: ProjectionConfig,
fusion: FusionConfig,
`export`: ExportConfig
`export`: ExportConfig,
jws: JWSConfig
)

object AppConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.acls.Acls
import ch.epfl.bluebrain.nexus.delta.sdk.fusion.FusionConfig
import ch.epfl.bluebrain.nexus.delta.sdk.http.StrictEntity
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.ComponentDescription.PluginDescription
import ch.epfl.bluebrain.nexus.delta.sdk.model._
Expand Down Expand Up @@ -126,6 +127,11 @@ class DeltaModule(appCfg: AppConfig, config: Config)(implicit classLoader: Class
List("@context", "@id", "@type", "reason", "details", "sourceId", "projectionId", "_total", "_results")
)
)

make[JWSPayloadHelper].from { config: AppConfig =>
JWSPayloadHelper(config.jws)
}

make[ActorSystem[Nothing]].fromResource { () =>
val make = IO.delay(
ActorSystem[Nothing](
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.Files.FilesLog
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.batch.{BatchCopy, BatchFiles}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.contexts.{files => fileCtxId}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model._
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes.{BatchFilesRoutes, DelegateFilesRoutes, FilesRoutes, TokenIssuer}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes.{BatchFilesRoutes, DelegateFilesRoutes, FilesRoutes}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.schemas.{files => filesSchemaId}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.{FileAttributesUpdateStream, Files}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.StoragesConfig.S3StorageConfig.DelegationConfig
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.StoragesConfig.{ShowFileLocation, StorageTypeConfig}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.contexts.{storages => storageCtxId, storagesMetadata => storageMetaCtxId}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model._
Expand All @@ -39,6 +38,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.fusion.FusionConfig
import ch.epfl.bluebrain.nexus.delta.sdk.http.HttpClient
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.jws.JWSPayloadHelper
import ch.epfl.bluebrain.nexus.delta.sdk.model._
import ch.epfl.bluebrain.nexus.delta.sdk.model.metrics.ScopedEventMetricEncoder
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.{Permissions, StoragePermissionProvider}
Expand All @@ -49,13 +49,9 @@ import ch.epfl.bluebrain.nexus.delta.sdk.sse.SseEncoder
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label
import ch.epfl.bluebrain.nexus.delta.sourcing.stream.Supervisor
import ch.epfl.bluebrain.nexus.delta.sourcing.{ScopedEventLog, Transactors}
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator
import com.typesafe.config.Config
import izumi.distage.model.definition.{Id, ModuleDef}

import java.security.interfaces.RSAPrivateCrtKey
import scala.concurrent.duration.DurationInt

/**
* Storages and Files wiring
*/
Expand Down Expand Up @@ -278,8 +274,8 @@ class StoragePluginModule(priority: Int) extends ModuleDef {

make[DelegateFilesRoutes].from {
(
cfg: StorageTypeConfig,
identities: Identities,
jwsPayloadHelper: JWSPayloadHelper,
aclCheck: AclCheck,
files: Files,
schemeDirectives: DeltaSchemeDirectives,
Expand All @@ -290,17 +286,11 @@ class StoragePluginModule(priority: Int) extends ModuleDef {
ordering: JsonKeyOrdering,
showLocation: ShowFileLocation
) =>
val delegationCfg = cfg.amazon
.flatMap(_.delegation)
.getOrElse(
DelegationConfig(new RSAKeyGenerator(2048).generate().toRSAPrivateKey.asInstanceOf[RSAPrivateCrtKey], 3.days)
)
val tokenIssuer = new TokenIssuer(delegationCfg.rsaKey, delegationCfg.tokenDuration)
new DelegateFilesRoutes(
identities,
aclCheck,
files,
tokenIssuer,
jwsPayloadHelper,
indexingAction(_, _, _)(shift),
schemeDirectives
)(baseUri, cr, ordering, showLocation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import ch.epfl.bluebrain.nexus.delta.sdk.syntax.httpResponseFieldsSyntax
import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag
import io.circe.syntax._
import io.circe.{Encoder, Json, JsonObject}
import io.circe.{Encoder, JsonObject}

/**
* Enumeration of File rejection types.
Expand Down Expand Up @@ -231,10 +231,6 @@ object FileRejection {
s"Linking or registering a file cannot be performed without a 'filename' or a 'path' that does not end with a filename."
)

final case object InvalidJWSPayload extends FileRejection("Signature missing, flattened JWS format expected")

final case class JWSSignatureExpired(payload: Json) extends FileRejection(s"Token expired for payload: $payload")

final case class CopyRejection(
sourceProj: ProjectRef,
destProject: ProjectRef,
Expand Down Expand Up @@ -285,7 +281,6 @@ object FileRejection {
case FetchRejection(_, _, FetchFileRejection.FileNotFound(_)) => (StatusCodes.InternalServerError, Seq.empty)
case SaveRejection(_, _, SaveFileRejection.ResourceAlreadyExists(_)) => (StatusCodes.Conflict, Seq.empty)
case SaveRejection(_, _, SaveFileRejection.BucketAccessDenied(_, _, _)) => (StatusCodes.Forbidden, Seq.empty)
case JWSSignatureExpired(_) => (StatusCodes.Forbidden, Seq.empty)
case CopyRejection(_, _, _, rejection) => (rejection.status, Seq.empty)
case FetchRejection(_, _, _) => (StatusCodes.InternalServerError, Seq.empty)
case SaveRejection(_, _, _) => (StatusCodes.InternalServerError, Seq.empty)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder}
import io.circe.syntax.EncoderOps
import io.circe.{Decoder, Encoder, Json}
import ch.epfl.bluebrain.nexus.delta.sdk.implicits._
import ch.epfl.bluebrain.nexus.delta.sdk.jws.JWSPayloadHelper
import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef

final class DelegateFilesRoutes(
identities: Identities,
aclCheck: AclCheck,
files: Files,
tokenIssuer: TokenIssuer,
jwsPayloadHelper: JWSPayloadHelper,
index: IndexingAction.Execute[File],
schemeDirectives: DeltaSchemeDirectives
)(implicit
Expand Down Expand Up @@ -84,7 +85,7 @@ final class DelegateFilesRoutes(
) =
for {
delegationResp <- files.delegate(project, desc, storageId)
jwsPayload <- tokenIssuer.issueJWSPayload(delegationResp.asJson)
jwsPayload <- jwsPayloadHelper.sign(delegationResp.asJson)
} yield jwsPayload

private def registerDelegatedFile(
Expand All @@ -94,7 +95,7 @@ final class DelegateFilesRoutes(
mode: IndexingMode
)(implicit c: Caller): IO[FileResource] =
for {
originalPayload <- tokenIssuer.verifyJWSPayload(jwsPayload)
originalPayload <- jwsPayloadHelper.verify(jwsPayload)
delegationResponse <- IO.fromEither(originalPayload.as[DelegationResponse])
fileId = FileId(delegationResponse.id, project)
fileResource <-
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,18 @@ package ch.epfl.bluebrain.nexus.delta.plugins.storage.storages
import akka.http.scaladsl.model.Uri
import cats.implicits._
import ch.epfl.bluebrain.nexus.delta.kernel.Secret
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes.TokenIssuer
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.StoragesConfig.S3StorageConfig.DelegationConfig
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.StoragesConfig.StorageTypeConfig
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model.{AbsolutePath, DigestAlgorithm, StorageType}
import ch.epfl.bluebrain.nexus.delta.sdk.auth.Credentials
import ch.epfl.bluebrain.nexus.delta.sdk.model.BaseUri
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.PaginationConfig
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission
import ch.epfl.bluebrain.nexus.delta.sourcing.config.EventLogConfig
import com.nimbusds.jose.jwk.RSAKey
import pureconfig.ConvertHelpers.{catchReadError, optF}
import pureconfig.error.{CannotConvert, ConfigReaderFailures, ConvertFailure, FailureReason}
import pureconfig.generic.auto._
import pureconfig.generic.semiauto.deriveReader
import pureconfig.{ConfigConvert, ConfigReader}

import java.security.interfaces.RSAPrivateCrtKey
import scala.concurrent.duration.FiniteDuration

/**
Expand Down Expand Up @@ -186,23 +181,11 @@ object StoragesConfig {
showLocation: Boolean,
defaultMaxFileSize: Long,
defaultBucket: String,
prefix: Option[Uri],
delegation: Option[DelegationConfig]
prefix: Option[Uri]
) extends StorageTypeEntryConfig {
val prefixUri: Uri = prefix.getOrElse(Uri.Empty)
}

object S3StorageConfig {
final case class DelegationConfig(privateKey: RSAPrivateCrtKey, tokenDuration: FiniteDuration) {
val rsaKey: RSAKey = TokenIssuer.generateRSAKeyFromPrivate(privateKey)
}

implicit val delegationReader: ConfigReader[DelegationConfig] = deriveReader

implicit private val privateKeyConvert: ConfigConvert[RSAPrivateCrtKey] =
ConfigConvert.viaStringTry[RSAPrivateCrtKey](TokenIssuer.parseRSAPrivateKey, _.toString)
}

/**
* Remote Disk storage configuration
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ trait StorageFixtures extends CirceLiteral {
implicit val config: StorageTypeConfig = StorageTypeConfig(
disk = DiskStorageConfig(diskVolume, Set(diskVolume,tmpVolume), DigestAlgorithm.default, permissions.read, permissions.write, showLocation = false, 50),
amazon = Some(S3StorageConfig("localhost", useDefaultCredentialProvider = false, Secret("my_key"), Secret("my_secret_key"),
permissions.read, permissions.write, showLocation = false, 60, defaultBucket = "potato", prefix = None, delegation = None)),
permissions.read, permissions.write, showLocation = false, 60, defaultBucket = "potato", prefix = None)),
remoteDisk = Some(RemoteDiskStorageConfig(DigestAlgorithm.default, BaseUri("http://localhost", Label.unsafe("v1")), Anonymous, permissions.read, permissions.write, showLocation = false, 70, 50.millis)),
)
implicit val showLocation: StoragesConfig.ShowFileLocation = config.showFileLocation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ object LocalStackS3StorageClient {
showLocation = false,
defaultMaxFileSize = 1,
defaultBucket = defaultBucket,
prefix = Some(prefix),
delegation = None
prefix = Some(prefix)
)
(S3StorageClient.unsafe(client), client, conf)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package ch.epfl.bluebrain.nexus.delta.sdk.jws

import ch.epfl.bluebrain.nexus.delta.plugins.storage.jws.RSAUtils
import com.nimbusds.jose.jwk.RSAKey
import pureconfig.generic.auto._
import pureconfig.generic.semiauto.deriveReader
import pureconfig.{ConfigConvert, ConfigReader}

import java.security.interfaces.RSAPrivateCrtKey
import scala.concurrent.duration.FiniteDuration

sealed trait JWSConfig

object JWSConfig {
final case object Disabled extends JWSConfig

final case class Enabled(privateKey: RSAPrivateCrtKey, ttl: FiniteDuration) extends JWSConfig {
val rsaKey: RSAKey = RSAUtils.generateRSAKeyFromPrivate(privateKey)
}

implicit private val privateKeyConvert: ConfigConvert[RSAPrivateCrtKey] =
ConfigConvert.viaStringTry[RSAPrivateCrtKey](RSAUtils.parseRSAPrivateKey, _.toString)

implicit val delegationReader: ConfigReader[JWSConfig] = deriveReader
}
Loading

0 comments on commit 6a9272b

Please sign in to comment.