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

Fix delegation + add the ability to tag at creation #5091

Merged
merged 1 commit into from
Aug 9, 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
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileCommand._
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileEvent._
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection._
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model._
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes.DelegateFilesRoutes.DelegationResponse
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.schemas.{files => fileSchema}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model.StorageRejection.{StorageFetchRejection, StorageIsDeprecated}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model.{DigestAlgorithm, Storage, StorageRejection, StorageType}
Expand Down Expand Up @@ -181,17 +180,21 @@ final class Files(
* @param storageId
* the optional storage identifier to expand as the id of the storage. When None, the default storage is used
*/
def delegate(projectRef: ProjectRef, description: FileDescription, storageId: Option[IdSegment])(implicit
def delegate(
projectRef: ProjectRef,
description: FileDescription,
storageId: Option[IdSegment],
tag: Option[UserTag]
)(implicit
caller: Caller
): IO[DelegationResponse] = {
): IO[FileDelegationRequest] = {
for {
pc <- fetchContext.onCreate(projectRef)
iri <- generateId(pc)
_ <-
test(CreateFile(iri, projectRef, testStorageRef, testStorageType, testAttributes, caller.subject, tag = None))
_ <- test(CreateFile(iri, projectRef, testStorageRef, testStorageType, testAttributes, caller.subject, tag = tag))
(_, storage) <- fetchAndValidateActiveStorage(storageId, projectRef, pc)
metadata <- fileOperations.delegate(storage, description.filename)
} yield DelegationResponse(metadata.bucket, iri, metadata.path, description.metadata, description.mediaType)
} yield FileDelegationRequest(storage.id, metadata.bucket, projectRef, iri, metadata.path, description, tag)
}.span("delegate")

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model

import akka.http.scaladsl.model.Uri
import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri
import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag
import io.circe.Codec
import ch.epfl.bluebrain.nexus.delta.sdk.implicits._
import io.circe.generic.extras.Configuration
import io.circe.generic.extras.semiauto.deriveConfiguredCodec

final case class FileDelegationRequest(
storageId: Iri,
bucket: String,
project: ProjectRef,
id: Iri,
path: Uri,
description: FileDescription,
tag: Option[UserTag]
)

object FileDelegationRequest {
implicit private val config: Configuration = Configuration.default
implicit val codec: Codec[FileDelegationRequest] = deriveConfiguredCodec
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ package ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model
import akka.http.scaladsl.model.ContentType
import cats.implicits.catsSyntaxOptionId
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.UploadedFileInformation
import io.circe.Codec
import io.circe.generic.extras.Configuration
import ch.epfl.bluebrain.nexus.delta.sdk.implicits._
import io.circe.generic.extras.semiauto.deriveConfiguredCodec

case class FileDescription(
filename: String,
Expand Down Expand Up @@ -39,4 +43,7 @@ object FileDescription {
)
}

implicit private val config: Configuration = Configuration.default
implicit val fileDescriptionCodec: Codec[FileDescription] = deriveConfiguredCodec[FileDescription]

}
Original file line number Diff line number Diff line change
@@ -1,35 +1,29 @@
package ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes

import akka.http.scaladsl.model.StatusCodes.{Created, OK}
import akka.http.scaladsl.model.{ContentType, Uri}
import akka.http.scaladsl.server._
import cats.effect.IO
import cats.syntax.all._
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.FileRejection._
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model._
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes.DelegateFilesRoutes._
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes.FileUriDirectives._
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.{FileResource, Files}
import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.StoragesConfig.ShowFileLocation
import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution
import ch.epfl.bluebrain.nexus.delta.rdf.utils.JsonKeyOrdering
import ch.epfl.bluebrain.nexus.delta.sdk.{IndexingAction, IndexingMode}
import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck
import ch.epfl.bluebrain.nexus.delta.sdk.circe.{CirceMarshalling, CirceUnmarshalling}
import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._
import ch.epfl.bluebrain.nexus.delta.sdk.directives.{AuthDirectives, ResponseToJsonLd}
import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities
import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, IdSegment}
import io.circe.generic.extras.Configuration
import io.circe.generic.extras.semiauto.deriveConfiguredDecoder
import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder}
import io.circe.syntax.EncoderOps
import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes.FileUriDirectives._
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.sdk.model.{BaseUri, IdSegment}
import ch.epfl.bluebrain.nexus.delta.sdk.{IndexingAction, IndexingMode}
import ch.epfl.bluebrain.nexus.delta.sourcing.model.ProjectRef
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Tag.UserTag
import io.circe.Json
import io.circe.syntax.EncoderOps

final class DelegateFilesRoutes(
identities: Identities,
Expand All @@ -50,73 +44,55 @@ final class DelegateFilesRoutes(
baseUriPrefix(baseUri.prefix) {
pathPrefix("delegate" / "files") {
extractCaller { implicit caller =>
projectRef { project =>
concat(
pathPrefix("validate") {
(pathEndOrSingleSlash & post) {
storageParam { storageId =>
entity(as[FileDescription]) { desc =>
emit(OK, validateFileDetails(project, storageId, desc).attemptNarrow[FileRejection])
}
}
concat(
(projectRef & pathPrefix("generate") & storageParam & tagParam & pathEndOrSingleSlash & post) {
(project, storageId, tag) =>
entity(as[FileDescription]) { desc =>
emit(OK, validateFileDetails(project, storageId, desc, tag).attemptNarrow[FileRejection])
}
},
(pathEndOrSingleSlash & post) {
(storageParam & indexingMode) { (storageId, mode) =>
entity(as[Json]) { jwsPayload =>
emit(
Created,
linkDelegatedFile(jwsPayload, project, storageId, mode)
.attemptNarrow[FileRejection]: ResponseToJsonLd
)
}
}
}
)
}
},
(pathPrefix("submit") & put & pathEndOrSingleSlash & entity(as[Json]) & indexingMode) {
(jwsPayload, mode) =>
emit(
Created,
linkDelegatedFile(jwsPayload, mode).attemptNarrow[FileRejection]: ResponseToJsonLd
)
}
)
}
}
}

private def validateFileDetails(project: ProjectRef, storageId: Option[IdSegment], desc: FileDescription)(implicit
c: Caller
) =
private def validateFileDetails(
project: ProjectRef,
storageId: Option[IdSegment],
desc: FileDescription,
tag: Option[UserTag]
)(implicit c: Caller) =
for {
delegationResp <- files.delegate(project, desc, storageId)
jwsPayload <- jwsPayloadHelper.sign(delegationResp.asJson)
delegationRequest <- files.delegate(project, desc, storageId, tag)
jwsPayload <- jwsPayloadHelper.sign(delegationRequest.asJson)
} yield jwsPayload

private def linkDelegatedFile(
jwsPayload: Json,
project: ProjectRef,
storageId: Option[IdSegment],
mode: IndexingMode
)(implicit c: Caller): IO[FileResource] =
for {
originalPayload <- jwsPayloadHelper.verify(jwsPayload)
delegationResponse <- IO.fromEither(originalPayload.as[DelegationResponse])
request = FileLinkRequest(delegationResponse.path.path, delegationResponse.mediaType, delegationResponse.metadata)
fileResource <- files.linkFile(Some(delegationResponse.id), project, storageId, request, None)
_ <- index(project, fileResource, mode)
originalPayload <- jwsPayloadHelper.verify(jwsPayload)
delegationRequest <- IO.fromEither(originalPayload.as[FileDelegationRequest])
linkRequest = FileLinkRequest(
delegationRequest.path.path,
delegationRequest.description.mediaType,
delegationRequest.description.metadata
)
fileResource <- files.linkFile(
Some(delegationRequest.id),
delegationRequest.project,
Some(delegationRequest.storageId),
linkRequest,
delegationRequest.tag
)
_ <- index(delegationRequest.project, fileResource, mode)
} yield fileResource

}

object DelegateFilesRoutes {

final case class DelegationResponse(
bucket: String,
id: Iri,
path: Uri,
metadata: Option[FileCustomMetadata],
mediaType: Option[ContentType]
)

object DelegationResponse {
implicit val enc: Encoder[DelegationResponse] = deriveEncoder
implicit val dec: Decoder[DelegationResponse] = deriveDecoder
}

implicit private val config: Configuration = Configuration.default
implicit val dec: Decoder[FileDescription] = deriveConfiguredDecoder[FileDescription]
}
Original file line number Diff line number Diff line change
Expand Up @@ -425,14 +425,14 @@ class S3StorageSpec extends StorageSpec {
for {
jwsPayload <-
deltaClient
.postAndReturn[Json](s"/delegate/files/$projectRef/validate?storage=nxv:$storageId", payload, Coyote) {
.postAndReturn[Json](s"/delegate/files/$projectRef/generate?storage=nxv:$storageId", payload, Coyote) {
expectOk
}
resp <- parseDelegationResponse(jwsPayload)
DelegationResponse(id, path, returnedBucket) = resp
_ = returnedBucket shouldEqual bucket
_ <- uploadLogoFileToS3(path)
_ <- deltaClient.post[Json](s"/delegate/files/$projectRef?storage=nxv:$storageId", jwsPayload, Coyote) {
_ <- deltaClient.put[Json](s"/delegate/files/submit", jwsPayload, Coyote) {
expectCreated
}
encodedId = UrlUtils.encode(id)
Expand Down