From 3be33d22476484e42bc38db188a64c36fb99f85c Mon Sep 17 00:00:00 2001 From: Daniel Bell Date: Mon, 9 Oct 2023 11:47:36 +0200 Subject: [PATCH] Ensure file errors are writen correctly --- .../delta/plugins/storage/files/Files.scala | 325 +++++++++++------- .../test/resources/files/errors/blank-id.json | 5 + .../files/routes/FilesRoutesSpec.scala | 138 ++++++-- .../nexus/delta/sdk/JsonLdValue.scala | 19 + .../sdk/directives/ResponseToJsonLd.scala | 9 +- .../delta/sdk/error/AuthTokenError.scala | 8 - 6 files changed, 344 insertions(+), 160 deletions(-) create mode 100644 delta/plugins/storage/src/test/resources/files/errors/blank-id.json diff --git a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/Files.scala b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/Files.scala index 48a69042ad..912b1fd738 100644 --- a/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/Files.scala +++ b/delta/plugins/storage/src/main/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/Files.scala @@ -54,27 +54,7 @@ import java.util.UUID /** * Operations for handling files */ -final class Files( - formDataExtractor: FormDataExtractor, - log: FilesLog, - aclCheck: AclCheck, - fetchContext: FetchContext[FileRejection], - storages: Storages, - storagesStatistics: StoragesStatistics, - remoteDiskStorageClient: RemoteDiskStorageClient, - config: StorageTypeConfig -)(implicit - uuidF: UUIDF, - system: ClassicActorSystem -) { - - implicit private val kamonComponent: KamonMetricComponent = KamonMetricComponent(entityType.value) - - // format: off - private val testStorageRef = ResourceRef.Revision(iri"http://localhost/test", 1) - private val testStorageType = StorageType.DiskStorage - private val testAttributes = FileAttributes(UUID.randomUUID(), "http://localhost", Uri.Path.Empty, "", None, 0, ComputedDigest(DigestAlgorithm.default, "value"), Client) - // format: on +trait Files { /** * Create a new file where the id is self generated @@ -90,16 +70,7 @@ final class Files( storageId: Option[IdSegment], projectRef: ProjectRef, entity: HttpEntity - )(implicit caller: Caller): IO[FileRejection, FileResource] = { - for { - pc <- fetchContext.onCreate(projectRef) - iri <- generateId(pc) - _ <- test(CreateFile(iri, projectRef, testStorageRef, testStorageType, testAttributes, caller.subject)) - (storageRef, storage) <- fetchActiveStorage(storageId, projectRef, pc) - attributes <- extractFileAttributes(iri, entity, storage) - res <- eval(CreateFile(iri, projectRef, storageRef, storage.tpe, attributes, caller.subject)) - } yield res - }.span("createFile") + )(implicit caller: Caller): IO[FileRejection, FileResource] /** * Create a new file with the provided id @@ -118,16 +89,7 @@ final class Files( storageId: Option[IdSegment], projectRef: ProjectRef, entity: HttpEntity - )(implicit caller: Caller): IO[FileRejection, FileResource] = { - for { - pc <- fetchContext.onCreate(projectRef) - iri <- expandIri(id, pc) - _ <- test(CreateFile(iri, projectRef, testStorageRef, testStorageType, testAttributes, caller.subject)) - (storageRef, storage) <- fetchActiveStorage(storageId, projectRef, pc) - attributes <- extractFileAttributes(iri, entity, storage) - res <- eval(CreateFile(iri, projectRef, storageRef, storage.tpe, attributes, caller.subject)) - } yield res - }.span("createFile") + )(implicit caller: Caller): IO[FileRejection, FileResource] /** * Create a new file linking where the id is self generated @@ -149,13 +111,7 @@ final class Files( filename: Option[String], mediaType: Option[ContentType], path: Uri.Path - )(implicit caller: Caller): IO[FileRejection, FileResource] = { - for { - pc <- fetchContext.onCreate(projectRef) - iri <- generateId(pc) - res <- createLink(iri, projectRef, pc, storageId, filename, mediaType, path) - } yield res - }.span("createLink") + )(implicit caller: Caller): IO[FileRejection, FileResource] /** * Create a new file linking it from an existing file in a storage @@ -180,13 +136,7 @@ final class Files( filename: Option[String], mediaType: Option[ContentType], path: Uri.Path - )(implicit caller: Caller): IO[FileRejection, FileResource] = { - for { - pc <- fetchContext.onCreate(projectRef) - iri <- expandIri(id, pc) - res <- createLink(iri, projectRef, pc, storageId, filename, mediaType, path) - } yield res - }.span("createLink") + )(implicit caller: Caller): IO[FileRejection, FileResource] /** * Update an existing file @@ -208,16 +158,7 @@ final class Files( projectRef: ProjectRef, rev: Int, entity: HttpEntity - )(implicit caller: Caller): IO[FileRejection, FileResource] = { - for { - pc <- fetchContext.onModify(projectRef) - iri <- expandIri(id, pc) - _ <- test(UpdateFile(iri, projectRef, testStorageRef, testStorageType, testAttributes, rev, caller.subject)) - (storageRef, storage) <- fetchActiveStorage(storageId, projectRef, pc) - attributes <- extractFileAttributes(iri, entity, storage) - res <- eval(UpdateFile(iri, projectRef, storageRef, storage.tpe, attributes, rev, caller.subject)) - } yield res - }.span("updateFile") + )(implicit caller: Caller): IO[FileRejection, FileResource] /** * Update a new file linking it from an existing file in a storage @@ -245,20 +186,7 @@ final class Files( mediaType: Option[ContentType], path: Uri.Path, rev: Int - )(implicit caller: Caller): IO[FileRejection, FileResource] = { - for { - pc <- fetchContext.onModify(projectRef) - iri <- expandIri(id, pc) - _ <- test(UpdateFile(iri, projectRef, testStorageRef, testStorageType, testAttributes, rev, caller.subject)) - (storageRef, storage) <- fetchActiveStorage(storageId, projectRef, pc) - resolvedFilename <- IO.fromOption(filename.orElse(path.lastSegment), InvalidFileLink(iri)) - description <- FileDescription(resolvedFilename, mediaType) - attributes <- LinkFile(storage, remoteDiskStorageClient, config) - .apply(path, description) - .mapError(LinkRejection(iri, storage.id, _)) - res <- eval(UpdateFile(iri, projectRef, storageRef, storage.tpe, attributes, rev, caller.subject)) - } yield res - }.span("updateLink") + )(implicit caller: Caller): IO[FileRejection, FileResource] /** * Add a tag to an existing file @@ -280,13 +208,7 @@ final class Files( tag: UserTag, tagRev: Int, rev: Int - )(implicit subject: Subject): IO[FileRejection, FileResource] = { - for { - pc <- fetchContext.onModify(projectRef) - iri <- expandIri(id, pc) - res <- eval(TagFile(iri, projectRef, tagRev, tag, rev, subject)) - } yield res - }.span("tagFile") + )(implicit subject: Subject): IO[FileRejection, FileResource] /** * Delete a tag on an existing file. @@ -305,13 +227,7 @@ final class Files( projectRef: ProjectRef, tag: UserTag, rev: Int - )(implicit subject: Subject): IO[FileRejection, FileResource] = { - for { - pc <- fetchContext.onModify(projectRef) - iri <- expandIri(id, pc) - res <- eval(DeleteFileTag(iri, projectRef, tag, rev, subject)) - } yield res - }.span("deleteFileTag") + )(implicit subject: Subject): IO[FileRejection, FileResource] /** * Deprecate an existing file @@ -327,13 +243,7 @@ final class Files( id: IdSegment, projectRef: ProjectRef, rev: Int - )(implicit subject: Subject): IO[FileRejection, FileResource] = { - for { - pc <- fetchContext.onModify(projectRef) - iri <- expandIri(id, pc) - res <- eval(DeprecateFile(iri, projectRef, rev, subject)) - } yield res - }.span("deprecateFile") + )(implicit subject: Subject): IO[FileRejection, FileResource] /** * Fetch the last version of a file content @@ -343,7 +253,196 @@ final class Files( * @param project * the project where the storage belongs */ - def fetchContent(id: IdSegmentRef, project: ProjectRef)(implicit caller: Caller): IO[FileRejection, FileResponse] = { + def fetchContent(id: IdSegmentRef, project: ProjectRef)(implicit caller: Caller): IO[FileRejection, FileResponse] + + /** + * Fetch the last version of a file + * + * @param id + * the identifier that will be expanded to the Iri of the file with its optional rev/tag + * @param project + * the project where the storage belongs + */ + def fetch(id: IdSegmentRef, project: ProjectRef): IO[FileRejection, FileResource] + + /** + * Starts a stream that attempts to update file attributes asynchronously for linked files in remote storages + * + * @param offset + * the offset to start from + * @return + */ + private[files] def attributesUpdateStream(offset: Offset): ElemStream[Unit] + + private[files] def updateAttributes(iri: Iri, project: ProjectRef): IO[FileRejection, Unit] +} + +final private class FilesImpl( + formDataExtractor: FormDataExtractor, + log: FilesLog, + aclCheck: AclCheck, + fetchContext: FetchContext[FileRejection], + storages: Storages, + storagesStatistics: StoragesStatistics, + remoteDiskStorageClient: RemoteDiskStorageClient, + config: StorageTypeConfig +)(implicit + uuidF: UUIDF, + system: ClassicActorSystem +) extends Files { + + implicit private val kamonComponent: KamonMetricComponent = KamonMetricComponent(entityType.value) + + private val logger: Logger = Logger[Files] + + // format: off + private val testStorageRef = ResourceRef.Revision(iri"http://localhost/test", 1) + private val testStorageType = StorageType.DiskStorage + private val testAttributes = FileAttributes(UUID.randomUUID(), "http://localhost", Uri.Path.Empty, "", None, 0, ComputedDigest(DigestAlgorithm.default, "value"), Client) + // format: on + + override def create( + storageId: Option[IdSegment], + projectRef: ProjectRef, + entity: HttpEntity + )(implicit caller: Caller): IO[FileRejection, FileResource] = { + for { + pc <- fetchContext.onCreate(projectRef) + iri <- generateId(pc) + _ <- test(CreateFile(iri, projectRef, testStorageRef, testStorageType, testAttributes, caller.subject)) + (storageRef, storage) <- fetchActiveStorage(storageId, projectRef, pc) + attributes <- extractFileAttributes(iri, entity, storage) + res <- eval(CreateFile(iri, projectRef, storageRef, storage.tpe, attributes, caller.subject)) + } yield res + }.span("createFile") + + override def create( + id: IdSegment, + storageId: Option[IdSegment], + projectRef: ProjectRef, + entity: HttpEntity + )(implicit caller: Caller): IO[FileRejection, FileResource] = { + for { + pc <- fetchContext.onCreate(projectRef) + iri <- expandIri(id, pc) + _ <- test(CreateFile(iri, projectRef, testStorageRef, testStorageType, testAttributes, caller.subject)) + (storageRef, storage) <- fetchActiveStorage(storageId, projectRef, pc) + attributes <- extractFileAttributes(iri, entity, storage) + res <- eval(CreateFile(iri, projectRef, storageRef, storage.tpe, attributes, caller.subject)) + } yield res + }.span("createFile") + + override def createLink( + storageId: Option[IdSegment], + projectRef: ProjectRef, + filename: Option[String], + mediaType: Option[ContentType], + path: Uri.Path + )(implicit caller: Caller): IO[FileRejection, FileResource] = { + for { + pc <- fetchContext.onCreate(projectRef) + iri <- generateId(pc) + res <- createLink(iri, projectRef, pc, storageId, filename, mediaType, path) + } yield res + }.span("createLink") + + override def createLink( + id: IdSegment, + storageId: Option[IdSegment], + projectRef: ProjectRef, + filename: Option[String], + mediaType: Option[ContentType], + path: Uri.Path + )(implicit caller: Caller): IO[FileRejection, FileResource] = { + for { + pc <- fetchContext.onCreate(projectRef) + iri <- expandIri(id, pc) + res <- createLink(iri, projectRef, pc, storageId, filename, mediaType, path) + } yield res + }.span("createLink") + + override def update( + id: IdSegment, + storageId: Option[IdSegment], + projectRef: ProjectRef, + rev: Int, + entity: HttpEntity + )(implicit caller: Caller): IO[FileRejection, FileResource] = { + for { + pc <- fetchContext.onModify(projectRef) + iri <- expandIri(id, pc) + _ <- test(UpdateFile(iri, projectRef, testStorageRef, testStorageType, testAttributes, rev, caller.subject)) + (storageRef, storage) <- fetchActiveStorage(storageId, projectRef, pc) + attributes <- extractFileAttributes(iri, entity, storage) + res <- eval(UpdateFile(iri, projectRef, storageRef, storage.tpe, attributes, rev, caller.subject)) + } yield res + }.span("updateFile") + + override def updateLink( + id: IdSegment, + storageId: Option[IdSegment], + projectRef: ProjectRef, + filename: Option[String], + mediaType: Option[ContentType], + path: Uri.Path, + rev: Int + )(implicit caller: Caller): IO[FileRejection, FileResource] = { + for { + pc <- fetchContext.onModify(projectRef) + iri <- expandIri(id, pc) + _ <- test(UpdateFile(iri, projectRef, testStorageRef, testStorageType, testAttributes, rev, caller.subject)) + (storageRef, storage) <- fetchActiveStorage(storageId, projectRef, pc) + resolvedFilename <- IO.fromOption(filename.orElse(path.lastSegment), InvalidFileLink(iri)) + description <- FileDescription(resolvedFilename, mediaType) + attributes <- LinkFile(storage, remoteDiskStorageClient, config) + .apply(path, description) + .mapError(LinkRejection(iri, storage.id, _)) + res <- eval(UpdateFile(iri, projectRef, storageRef, storage.tpe, attributes, rev, caller.subject)) + } yield res + }.span("updateLink") + + override def tag( + id: IdSegment, + projectRef: ProjectRef, + tag: UserTag, + tagRev: Int, + rev: Int + )(implicit subject: Subject): IO[FileRejection, FileResource] = { + for { + pc <- fetchContext.onModify(projectRef) + iri <- expandIri(id, pc) + res <- eval(TagFile(iri, projectRef, tagRev, tag, rev, subject)) + } yield res + }.span("tagFile") + + override def deleteTag( + id: IdSegment, + projectRef: ProjectRef, + tag: UserTag, + rev: Int + )(implicit subject: Subject): IO[FileRejection, FileResource] = { + for { + pc <- fetchContext.onModify(projectRef) + iri <- expandIri(id, pc) + res <- eval(DeleteFileTag(iri, projectRef, tag, rev, subject)) + } yield res + }.span("deleteFileTag") + + override def deprecate( + id: IdSegment, + projectRef: ProjectRef, + rev: Int + )(implicit subject: Subject): IO[FileRejection, FileResource] = { + for { + pc <- fetchContext.onModify(projectRef) + iri <- expandIri(id, pc) + res <- eval(DeprecateFile(iri, projectRef, rev, subject)) + } yield res + }.span("deprecateFile") + + override def fetchContent(id: IdSegmentRef, project: ProjectRef)(implicit + caller: Caller + ): IO[FileRejection, FileResponse] = { for { file <- fetch(id, project) attributes = file.value.attributes @@ -358,15 +457,7 @@ final class Files( } yield FileResponse(attributes.filename, mediaType, attributes.bytes, s) }.span("fetchFileContent") - /** - * Fetch the last version of a file - * - * @param id - * the identifier that will be expanded to the Iri of the file with its optional rev/tag - * @param project - * the project where the storage belongs - */ - def fetch(id: IdSegmentRef, project: ProjectRef): IO[FileRejection, FileResource] = { + override def fetch(id: IdSegmentRef, project: ProjectRef): IO[FileRejection, FileResource] = { for { pc <- fetchContext.onRead(project) iri <- expandIri(id.value, pc) @@ -448,13 +539,7 @@ final class Files( private def generateId(pc: ProjectContext)(implicit uuidF: UUIDF): UIO[Iri] = uuidF().map(uuid => pc.base.iri / uuid.toString) - /** - * Starts a stream that attempts to update file attributes asynchronously for linked files in remote storages - * @param offset - * the offset to start from - * @return - */ - private[files] def attributesUpdateStream(offset: Offset): ElemStream[Unit] = { + override private[files] def attributesUpdateStream(offset: Offset): ElemStream[Unit] = { for { // The stream will start only if remote storage is enabled retryStrategy <- Stream.iterable(config.remoteDisk).map { c => @@ -515,7 +600,7 @@ final class Files( } yield stream } - private[files] def updateAttributes(iri: Iri, project: ProjectRef): IO[FileRejection, Unit] = + override private[files] def updateAttributes(iri: Iri, project: ProjectRef): IO[FileRejection, Unit] = for { f <- log.stateOr(project, iri, FileNotFound(iri, project)) storage <- storages @@ -543,8 +628,6 @@ final class Files( object Files { - private val logger: Logger = Logger[Files] - /** * The file entity type. */ @@ -719,7 +802,7 @@ object Files { as: ActorSystem[Nothing] ): Files = { implicit val classicAs: ClassicActorSystem = as.classicSystem - new Files( + new FilesImpl( FormDataExtractor(config.mediaTypeDetector), ScopedEventLog(definition, config.eventLog, xas), aclCheck, diff --git a/delta/plugins/storage/src/test/resources/files/errors/blank-id.json b/delta/plugins/storage/src/test/resources/files/errors/blank-id.json new file mode 100644 index 0000000000..66a095e9d3 --- /dev/null +++ b/delta/plugins/storage/src/test/resources/files/errors/blank-id.json @@ -0,0 +1,5 @@ +{ + "@context" : "https://bluebrain.github.io/nexus/contexts/error.json", + "@type" : "BlankResourceId", + "reason" : "Resource identifier cannot be blank." +} diff --git a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala index 2d8cb6a800..1b3fea9960 100644 --- a/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala +++ b/delta/plugins/storage/src/test/scala/ch/epfl/bluebrain/nexus/delta/plugins/storage/files/routes/FilesRoutesSpec.scala @@ -5,38 +5,42 @@ import akka.http.scaladsl.model.ContentTypes.`text/plain(UTF-8)` import akka.http.scaladsl.model.MediaRanges._ import akka.http.scaladsl.model.MediaTypes.`text/html` import akka.http.scaladsl.model.headers.{Accept, Location, OAuth2BearerToken, RawHeader} -import akka.http.scaladsl.model.{StatusCodes, Uri} +import akka.http.scaladsl.model._ import akka.http.scaladsl.server.Route import ch.epfl.bluebrain.nexus.delta.kernel.http.MediaTypeDetectorConfig import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.Digest.ComputedDigest import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.model.{FileAttributes, FileRejection} import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.routes.FilesRoutesSpec.fileMetadata -import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.{contexts => fileContexts, permissions, FileFixtures, Files, FilesConfig} +import ch.epfl.bluebrain.nexus.delta.plugins.storage.files.{FileFixtures, FileResource, Files, FilesConfig, permissions, contexts => fileContexts} import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.model.{StorageRejection, StorageStatEntry, StorageType} import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.operations.remote.client.RemoteDiskStorageClient -import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.{contexts => storageContexts, permissions => storagesPermissions, StorageFixtures, Storages, StoragesConfig, StoragesStatistics} +import ch.epfl.bluebrain.nexus.delta.plugins.storage.storages.{StorageFixtures, Storages, StoragesConfig, contexts => storageContexts, permissions => storagesPermissions} import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri import ch.epfl.bluebrain.nexus.delta.rdf.RdfMediaTypes.`application/ld+json` import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.{contexts, nxv} import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteContextResolution} +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder import ch.epfl.bluebrain.nexus.delta.sdk.IndexingAction import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclSimpleCheck import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.AclAddress import ch.epfl.bluebrain.nexus.delta.sdk.auth.{AuthTokenProvider, Credentials} -import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaSchemeDirectives +import ch.epfl.bluebrain.nexus.delta.sdk.directives.{DeltaSchemeDirectives, FileResponse} import ch.epfl.bluebrain.nexus.delta.sdk.http.HttpClient import ch.epfl.bluebrain.nexus.delta.sdk.identities.IdentitiesDummy import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.{Caller, ServiceAccount} import ch.epfl.bluebrain.nexus.delta.sdk.implicits._ -import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceUris} +import ch.epfl.bluebrain.nexus.delta.sdk.marshalling.HttpResponseFields +import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, IdSegment, IdSegmentRef, ResourceUris} import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.events import ch.epfl.bluebrain.nexus.delta.sdk.permissions.model.Permission import ch.epfl.bluebrain.nexus.delta.sdk.projects.FetchContextDummy import ch.epfl.bluebrain.nexus.delta.sdk.resolvers.ResolverContextResolution +import ch.epfl.bluebrain.nexus.delta.sdk.resources.model.ResourceRejection import ch.epfl.bluebrain.nexus.delta.sdk.utils.{BaseRouteSpec, RouteFixtures} import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group, Subject, User} -import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef, ResourceRef} +import ch.epfl.bluebrain.nexus.delta.sourcing.model.{Label, ProjectRef, ResourceRef, Tag} +import ch.epfl.bluebrain.nexus.delta.sourcing.offset.Offset import ch.epfl.bluebrain.nexus.testkit._ import ch.epfl.bluebrain.nexus.testkit.bio.IOFromMap import io.circe.Json @@ -53,8 +57,6 @@ class FilesRoutesSpec import akka.actor.typed.scaladsl.adapter._ implicit val typedSystem: typed.ActorSystem[Nothing] = system.toTyped val httpClient: HttpClient = HttpClient()(httpClientConfig, system, s) - val authTokenProvider: AuthTokenProvider = AuthTokenProvider.anonymousForTest - val remoteDiskStorageClient = new RemoteDiskStorageClient(httpClient, authTokenProvider, Credentials.Anonymous) // TODO: sort out how we handle this in tests implicit override def rcr: RemoteContextResolution = @@ -70,7 +72,6 @@ class FilesRoutesSpec implicit private val caller: Caller = Caller(alice, Set(alice, Anonymous, Authenticated(realm), Group("group", realm))) - private val identities = IdentitiesDummy(caller) private val asAlice = addCredentials(OAuth2BearerToken("alice")) @@ -94,9 +95,6 @@ class FilesRoutesSpec private val stCfg = config.copy(disk = config.disk.copy(defaultMaxFileSize = 1000, allowedVolumes = Set(path))) - private val storagesStatistics: StoragesStatistics = - (_, _) => IO.pure { StorageStatEntry(0, 0) } - private val aclCheck = AclSimpleCheck().accepted lazy val storages: Storages = Storages( fetchContext.mapRejection(StorageRejection.ProjectContextRejection), @@ -107,21 +105,100 @@ class FilesRoutesSpec StoragesConfig(eventLogConfig, pagination, stCfg), ServiceAccount(User("nexus-sa", Label.unsafe("sa"))) ).accepted - lazy val files: Files = Files( - fetchContext.mapRejection(FileRejection.ProjectContextRejection), - aclCheck, - storages, - storagesStatistics, - xas, - config, - FilesConfig(eventLogConfig, MediaTypeDetectorConfig.Empty), - remoteDiskStorageClient - ) - private val groupDirectives = - DeltaSchemeDirectives(fetchContext, ioFromMap(uuid -> projectRef.organization), ioFromMap(uuid -> projectRef)) + + private def createFiles() = { + Files( + fetchContext.mapRejection(FileRejection.ProjectContextRejection), + aclCheck, + storages, + (_, _) => + IO.pure { + StorageStatEntry(0, 0) + }, + xas, + config, + FilesConfig(eventLogConfig, MediaTypeDetectorConfig.Empty), + new RemoteDiskStorageClient(httpClient, AuthTokenProvider.anonymousForTest, Credentials.Anonymous) + ) + } private lazy val routes = - Route.seal(FilesRoutes(stCfg, identities, aclCheck, files, groupDirectives, IndexingAction.noop)) + createRoutes(createFiles()) + + private def filesWithErrorInFileContents[E: JsonLdEncoder: HttpResponseFields](error: E): Files = { + new Files { + override def create(storageId: Option[IdSegment], projectRef: ProjectRef, entity: HttpEntity)(implicit + caller: Caller + ): IO[FileRejection, FileResource] = ??? + override def create(id: IdSegment, storageId: Option[IdSegment], projectRef: ProjectRef, entity: HttpEntity)( + implicit caller: Caller + ): IO[FileRejection, FileResource] = ??? + override def createLink( + storageId: Option[IdSegment], + projectRef: ProjectRef, + filename: Option[String], + mediaType: Option[ContentType], + path: Uri.Path + )(implicit caller: Caller): IO[FileRejection, FileResource] = ??? + override def createLink( + id: IdSegment, + storageId: Option[IdSegment], + projectRef: ProjectRef, + filename: Option[String], + mediaType: Option[ContentType], + path: Uri.Path + )(implicit caller: Caller): IO[FileRejection, FileResource] = ??? + override def update( + id: IdSegment, + storageId: Option[IdSegment], + projectRef: ProjectRef, + rev: Int, + entity: HttpEntity + )(implicit caller: Caller): IO[FileRejection, FileResource] = ??? + override def updateLink( + id: IdSegment, + storageId: Option[IdSegment], + projectRef: ProjectRef, + filename: Option[String], + mediaType: Option[ContentType], + path: Uri.Path, + rev: Int + )(implicit caller: Caller): IO[FileRejection, FileResource] = ??? + override def tag(id: IdSegment, projectRef: ProjectRef, tag: Tag.UserTag, tagRev: Int, rev: Int)(implicit + subject: Subject + ): IO[FileRejection, FileResource] = ??? + override def deleteTag(id: IdSegment, projectRef: ProjectRef, tag: Tag.UserTag, rev: Int)(implicit + subject: Subject + ): IO[FileRejection, FileResource] = ??? + override def deprecate(id: IdSegment, projectRef: ProjectRef, rev: Int)(implicit + subject: Subject + ): IO[FileRejection, FileResource] = ??? + override def fetchContent(id: IdSegmentRef, project: ProjectRef)(implicit + caller: Caller + ): IO[FileRejection, FileResponse] = IO.pure( + FileResponse( + "file.name", + ContentTypes.`text/plain(UTF-8)`, + 1024, + IO.raiseError(error) + ) + ) + override def fetch(id: IdSegmentRef, project: ProjectRef): IO[FileRejection, FileResource] = ??? + override private[files] def attributesUpdateStream(offset: Offset) = ??? + override private[files] def updateAttributes(iri: Iri, project: ProjectRef) = ??? + } + } + + private def createRoutes(files: Files) = Route.seal( + FilesRoutes( + stCfg, + IdentitiesDummy(caller), + aclCheck, + files, + DeltaSchemeDirectives(fetchContext, ioFromMap(uuid -> projectRef.organization), ioFromMap(uuid -> projectRef)), + IndexingAction.noop + ) + ) private val diskIdRev = ResourceRef.Revision(dId, 1) private val s3IdRev = ResourceRef.Revision(s3Id, 2) @@ -473,6 +550,17 @@ class FilesRoutesSpec response.header[Location].value.uri shouldEqual Uri("https://bbp.epfl.ch/nexus/web/org/project/resources/file1") } } + + "fail when auth fails" in { + val mockRoutes = createRoutes(filesWithErrorInFileContents[ResourceRejection](ResourceRejection.BlankResourceId)) + Get("/v1/files/org/proj/file1") ~> Accept(`*/*`) ~> mockRoutes ~> check { + contentType.value shouldEqual `application/ld+json`.value + response.asJson shouldEqual jsonContentOf( + "/files/errors/blank-id.json" + ) + status shouldEqual StatusCodes.BadRequest + } + } } } diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/JsonLdValue.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/JsonLdValue.scala index d1277fa9d3..aacffff40b 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/JsonLdValue.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/JsonLdValue.scala @@ -1,6 +1,11 @@ package ch.epfl.bluebrain.nexus.delta.sdk +import ch.epfl.bluebrain.nexus.delta.rdf.RdfError +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdOptions} +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.{CompactedJsonLd, ExpandedJsonLd} +import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteContextResolution} import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder +import monix.bio.IO /** * A definition of a value that can be converted to JSONLD @@ -24,4 +29,18 @@ object JsonLdValue { override val value: A = v override val encoder: JsonLdEncoder[A] = implicitly[JsonLdEncoder[A]] } + + implicit val jsonLdEncoder: JsonLdEncoder[JsonLdValue] = { + new JsonLdEncoder[JsonLdValue] { + override def context(value: JsonLdValue): ContextValue = value.encoder.context(value.value) + override def expand( + value: JsonLdValue + )(implicit opts: JsonLdOptions, api: JsonLdApi, rcr: RemoteContextResolution): IO[RdfError, ExpandedJsonLd] = + value.encoder.expand(value.value) + override def compact( + value: JsonLdValue + )(implicit opts: JsonLdOptions, api: JsonLdApi, rcr: RemoteContextResolution): IO[RdfError, CompactedJsonLd] = + value.encoder.compact(value.value) + } + } } diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/ResponseToJsonLd.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/ResponseToJsonLd.scala index 6dfde427e9..1b28a3a957 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/ResponseToJsonLd.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/directives/ResponseToJsonLd.scala @@ -106,12 +106,9 @@ object ResponseToJsonLd extends FileBytesInstances { override def apply(statusOverride: Option[StatusCode]): Route = { val flattened = io.flatMap { fr => fr.content.attempt.map(_.map { s => fr.metadata -> s }) }.attempt onSuccess(flattened.runToFuture) { - case Left(complete: Complete[E]) => emit(complete) - case Left(reject: Reject[E]) => emit(reject) - case Right(Left(c)) => - implicit val valueEncoder = c.value.encoder - emit(c.value.value) - + case Left(complete: Complete[E]) => emit(complete) + case Left(reject: Reject[E]) => emit(reject) + case Right(Left(c)) => emit(c) case Right(Right((metadata, content))) => headerValueByType(Accept) { accept => if (accept.mediaRanges.exists(_.matches(metadata.contentType.mediaType))) { diff --git a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/error/AuthTokenError.scala b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/error/AuthTokenError.scala index 2f1bd3e3f8..6ef19a4c2e 100644 --- a/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/error/AuthTokenError.scala +++ b/delta/sdk/src/main/scala/ch/epfl/bluebrain/nexus/delta/sdk/error/AuthTokenError.scala @@ -27,12 +27,6 @@ object AuthTokenError { final case class AuthTokenNotFoundInResponse(failure: DecodingFailure) extends AuthTokenError(s"Auth token not found in auth response: ${failure.reason}") - /** - * Signals that the expiry was missing from the authentication response - */ - final case class ExpiryNotFoundInResponse(failure: DecodingFailure) - extends AuthTokenError(s"Expiry not found in auth response: ${failure.reason}") - /** * Signals that the realm specified for authentication is deprecated */ @@ -45,8 +39,6 @@ object AuthTokenError { JsonObject(keywords.tpe := "AuthTokenHttpError", "reason" := r.reason) case AuthTokenNotFoundInResponse(r) => JsonObject(keywords.tpe -> "AuthTokenNotFoundInResponse".asJson, "reason" := r.message) - case ExpiryNotFoundInResponse(r) => - JsonObject(keywords.tpe -> "ExpiryNotFoundInResponse".asJson, "reason" := r.message) case r: RealmIsDeprecated => JsonObject(keywords.tpe := "RealmIsDeprecated", "reason" := r.getMessage) }