Skip to content

Commit

Permalink
feat: Add scopes to tokens issued by JwtService (DEV-3451) (#3178)
Browse files Browse the repository at this point in the history
Co-authored-by: Raitis Veinbahs <raitis.veinbahs@dasch.swiss>
  • Loading branch information
seakayone and siers committed Apr 10, 2024
1 parent 68de1c6 commit 73fc75f
Show file tree
Hide file tree
Showing 10 changed files with 676 additions and 136 deletions.
@@ -0,0 +1,123 @@
/*
* Copyright © 2021 - 2024 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors.
* SPDX-License-Identifier: Apache-2.0
*/

package org.knora.sipi

import zio.Task
import zio.ULayer
import zio.ZLayer
import zio.nio.file.Path

import org.knora.webapi.config.AppConfig
import org.knora.webapi.messages.store.sipimessages.DeleteTemporaryFileRequest
import org.knora.webapi.messages.store.sipimessages.IIIFServiceStatusResponse
import org.knora.webapi.messages.store.sipimessages.MoveTemporaryFileToPermanentStorageRequest
import org.knora.webapi.messages.store.sipimessages.SipiGetTextFileRequest
import org.knora.webapi.messages.store.sipimessages.SipiGetTextFileResponse
import org.knora.webapi.messages.v2.responder.SuccessResponseV2
import org.knora.webapi.routing.JwtService
import org.knora.webapi.slice.admin.api.model.MaintenanceRequests.AssetId
import org.knora.webapi.slice.admin.domain.model.KnoraProject
import org.knora.webapi.slice.admin.domain.model.User
import org.knora.webapi.slice.admin.domain.service.Asset
import org.knora.webapi.slice.admin.domain.service.DspIngestClient
import org.knora.webapi.store.iiif.api.FileMetadataSipiResponse
import org.knora.webapi.store.iiif.api.SipiService
import org.knora.webapi.store.iiif.impl.SipiServiceLive
import org.knora.webapi.store.iiif.impl.SipiServiceMock

case class WhichSipiService(useLive: Boolean)

object WhichSipiService {
val live: ULayer[WhichSipiService] = ZLayer.succeed(WhichSipiService(useLive = true))
val mock: ULayer[WhichSipiService] = ZLayer.succeed(WhichSipiService(useLive = false))
}

/**
* A delegator for the Sipi service, used in tests.
* Depending on the [[WhichSipiService]] configuration, it delegates to either the [[SipiServiceLive]] or the [[SipiServiceMock]].
*/
case class SipiServiceTestDelegator(
private val whichSipi: WhichSipiService,
private val live: SipiServiceLive,
private val mock: SipiServiceMock,
) extends SipiService {

private final val sipiService =
if (whichSipi.useLive) { live }
else { mock }

/**
* Asks Sipi for metadata about a file in the tmp folder, served from the 'knora.json' route.
*
* @param filename the path to the file.
* @return a [[FileMetadataSipiResponse]] containing the requested metadata.
*/
override def getFileMetadataFromSipiTemp(filename: String): Task[FileMetadataSipiResponse] =
sipiService.getFileMetadataFromSipiTemp(filename)

/**
* Asks DSP-Ingest for metadata about a file in permanent location, served from the 'knora.json' route.
*
* @param shortcode the shortcode of the project.
* @param assetId for the file.
* @return a [[FileMetadataSipiResponse]] containing the requested metadata.
*/
override def getFileMetadataFromDspIngest(
shortcode: KnoraProject.Shortcode,
assetId: AssetId,
): Task[FileMetadataSipiResponse] =
sipiService.getFileMetadataFromDspIngest(shortcode, assetId)

/**
* Asks Sipi to move a file from temporary storage to permanent storage.
*
* @param moveTemporaryFileToPermanentStorageRequestV2 the request.
* @return a [[SuccessResponseV2]].
*/
override def moveTemporaryFileToPermanentStorage(
moveTemporaryFileToPermanentStorageRequestV2: MoveTemporaryFileToPermanentStorageRequest,
): Task[SuccessResponseV2] =
sipiService.moveTemporaryFileToPermanentStorage(moveTemporaryFileToPermanentStorageRequestV2)

/**
* Asks Sipi to delete a temporary file.
*
* @param deleteTemporaryFileRequestV2 the request.
* @return a [[SuccessResponseV2]].
*/
override def deleteTemporaryFile(deleteTemporaryFileRequestV2: DeleteTemporaryFileRequest): Task[SuccessResponseV2] =
sipiService.deleteTemporaryFile(deleteTemporaryFileRequestV2)

/**
* Asks Sipi for a text file used internally by Knora.
*
* @param textFileRequest the request message.
*/
override def getTextFileRequest(textFileRequest: SipiGetTextFileRequest): Task[SipiGetTextFileResponse] =
sipiService.getTextFileRequest(textFileRequest)

/**
* Tries to access the IIIF Service.
*/
override def getStatus(): Task[IIIFServiceStatusResponse] =
sipiService.getStatus()

/**
* Downloads an asset from Sipi.
*
* @param asset The asset to download.
* @param targetDir The target directory in which the asset should be stored.
* @param user The user who is downloading the asset.
* @return The path to the downloaded asset. If the asset could not be downloaded, [[None]] is returned.
*/
override def downloadAsset(asset: Asset, targetDir: Path, user: User): Task[Option[Path]] =
sipiService.downloadAsset(asset, targetDir, user)
}

object SipiServiceTestDelegator {
val layer: ZLayer[AppConfig & DspIngestClient & JwtService & WhichSipiService, Nothing, SipiService] =
SipiServiceMock.layer >+> SipiServiceLive.layer >>> ZLayer.derive[SipiServiceTestDelegator]
}
78 changes: 77 additions & 1 deletion integration/src/test/scala/org/knora/sipi/SipiIT.scala
Expand Up @@ -24,12 +24,82 @@ import scala.util.Try
import org.knora.sipi.MockDspApiServer.verify._
import org.knora.webapi.config.AppConfig
import org.knora.webapi.messages.admin.responder.projectsmessages.PermissionCodeAndProjectRestrictedViewSettings
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.IriIdentifier
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.ShortcodeIdentifier
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.ShortnameIdentifier
import org.knora.webapi.messages.util.KnoraSystemInstances.Users.SystemUser
import org.knora.webapi.routing.JwtService
import org.knora.webapi.routing.JwtServiceLive
import org.knora.webapi.slice.admin.domain.model.KnoraProject
import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri
import org.knora.webapi.slice.admin.domain.service.KnoraProjectRepo
import org.knora.webapi.slice.admin.domain.service.KnoraProjectService
import org.knora.webapi.slice.common.repo.service.CrudRepository
import org.knora.webapi.store.cache.CacheService
import org.knora.webapi.testcontainers.SharedVolumes
import org.knora.webapi.testcontainers.SipiTestContainer

final case class KnoraProjectRepoInMemory(projects: Ref[Chunk[KnoraProject]])
extends AbstractInMemoryCrudRepository[KnoraProject, ProjectIri](projects, _.id)
with KnoraProjectRepo {

override def findById(id: ProjectIdentifierADM): Task[Option[KnoraProject]] = projects.get.map(
_.find(id match {
case ShortcodeIdentifier(shortcode) => _.shortcode == shortcode
case ShortnameIdentifier(shortname) => _.shortname == shortname
case IriIdentifier(iri) => _.id.value == iri.value
}),
)
}

abstract class AbstractInMemoryCrudRepository[Entity, Id](entities: Ref[Chunk[Entity]], getId: Entity => Id)
extends CrudRepository[Entity, Id] {

/**
* Saves a given entity. Use the returned instance for further operations as the save operation might have changed the entity instance completely.
*
* @param entity The entity to be saved.
* @return the saved entity.
*/
override def save(entity: Entity): Task[Entity] = entities.update(_.appended(entity)).as(entity)

/**
* Deletes a given entity.
*
* @param entity The entity to be deleted
*/
override def delete(entity: Entity): Task[Unit] = deleteById(getId(entity))

/**
* Deletes the entity with the given id.
* If the entity is not found in the persistence store it is silently ignored.
*
* @param id The identifier to the entity to be deleted
*/
override def deleteById(id: Id): Task[Unit] = entities.update(_.filterNot(getId(_) == id))

/**
* Retrieves an entity by its id.
*
* @param id The identifier of type [[Id]].
* @return the entity with the given id or [[None]] if none found.
*/
override def findById(id: Id): Task[Option[Entity]] = entities.get.map(_.find(getId(_) == id))

/**
* Returns all instances of the type.
*
* @return all instances of the type.
*/
override def findAll(): Task[Chunk[Entity]] = entities.get
}

object KnoraProjectRepoInMemory {
val layer: ULayer[KnoraProjectRepoInMemory] =
ZLayer.fromZIO(Ref.make(Chunk.empty[KnoraProject]).map(KnoraProjectRepoInMemory(_)))
}

object SipiIT extends ZIOSpecDefault {

private val imageTestfile = "FGiLaT4zzuV-CqwbEDFAFeS.jp2"
Expand All @@ -46,7 +116,13 @@ object SipiIT extends ZIOSpecDefault {
ZIO
.serviceWithZIO[JwtService](_.createJwt(SystemUser))
.map(_.jwtString)
.provide(JwtServiceLive.layer, AppConfig.layer)
.provide(
JwtServiceLive.layer,
AppConfig.layer,
KnoraProjectService.layer,
CacheService.layer,
KnoraProjectRepoInMemory.layer,
)

private val cookiesSuite =
suite("Given a request is authorized using cookies")(
Expand Down
30 changes: 14 additions & 16 deletions integration/src/test/scala/org/knora/webapi/core/LayersTest.scala
Expand Up @@ -8,9 +8,12 @@ package org.knora.webapi.core
import org.apache.pekko
import zio._

import org.knora.sipi.SipiServiceTestDelegator
import org.knora.sipi.WhichSipiService
import org.knora.webapi.config.AppConfig.AppConfigurations
import org.knora.webapi.config.AppConfig.AppConfigurationsTest
import org.knora.webapi.config.AppConfigForTestContainers
import org.knora.webapi.config.JwtConfig
import org.knora.webapi.messages.StringFormatter
import org.knora.webapi.messages.util._
import org.knora.webapi.messages.util.search.QueryTraverser
Expand Down Expand Up @@ -59,8 +62,6 @@ import org.knora.webapi.store.cache.CacheServiceRequestMessageHandlerLive
import org.knora.webapi.store.iiif.IIIFRequestMessageHandler
import org.knora.webapi.store.iiif.IIIFRequestMessageHandlerLive
import org.knora.webapi.store.iiif.api.SipiService
import org.knora.webapi.store.iiif.impl.SipiServiceLive
import org.knora.webapi.store.iiif.impl.SipiServiceMock
import org.knora.webapi.store.triplestore.api.TriplestoreService
import org.knora.webapi.store.triplestore.impl.TriplestoreServiceLive
import org.knora.webapi.store.triplestore.upgrade.RepositoryUpdater
Expand All @@ -82,7 +83,7 @@ object LayersTest {
DefaultTestEnvironmentWithoutSipi & SipiTestContainer & DspIngestTestContainer & SharedVolumes.Images

type CommonR0 =
pekko.actor.ActorSystem & AppConfigurationsTest & JwtService & SipiService & StringFormatter
pekko.actor.ActorSystem & AppConfigurationsTest & JwtConfig & WhichSipiService

type CommonR =
ApiRoutes & AdminApiEndpoints & ApiV2Endpoints & AppRouter & AssetPermissionsResponder & Authenticator &
Expand All @@ -94,12 +95,12 @@ object LayersTest {
ProjectExportStorageService & ProjectImportService & ProjectService & ProjectRestService & QueryTraverser &
RepositoryUpdater & ResourceUtilV2 & ResourcesResponderV2 & RestCardinalityService & SearchApiRoutes &
SearchResponderV2 & StandoffResponderV2 & StandoffTagUtilV2 & State & TestClientService & TriplestoreService &
UserService & UsersResponder & UsersRestService & ValuesResponderV2
UserService & UsersResponder & UsersRestService & ValuesResponderV2 & JwtService & SipiService & StringFormatter

private val commonLayersForAllIntegrationTests =
ZLayer.makeSome[CommonR0, CommonR](
AdminModule.layer,
AdminApiModule.layer,
AdminModule.layer,
ApiRoutes.layer,
ApiV2Endpoints.layer,
AppRouter.layer,
Expand All @@ -122,10 +123,12 @@ object LayersTest {
InferenceOptimizationService.layer,
IriConverter.layer,
IriService.layer,
JwtServiceLive.layer,
KnoraResponseRenderer.layer,
KnoraUserToUserConverter.layer,
ListsResponder.layer,
ListsResponderV2.layer,
ManagementEndpoints.layer,
ManagementRoutes.layer,
MessageRelayLive.layer,
OntologyCacheLive.layer,
Expand All @@ -149,41 +152,36 @@ object LayersTest {
SearchApiRoutes.layer,
SearchEndpoints.layer,
SearchResponderV2Live.layer,
SipiServiceTestDelegator.layer,
StandoffResponderV2.layer,
StandoffTagUtilV2Live.layer,
State.layer,
StringFormatter.live,
TapirToPekkoInterpreter.layer,
TestClientService.layer,
TriplestoreServiceLive.layer,
UserService.layer,
UsersResponder.layer,
ValuesResponderV2Live.layer,
ManagementEndpoints.layer,
)

private val fusekiAndSipiTestcontainers =
ZLayer.make[
AppConfigurations & DspIngestTestContainer & FusekiTestContainer & JwtService & SharedVolumes.Images &
SipiService & SipiTestContainer & StringFormatter,
AppConfigurations & DspIngestTestContainer & FusekiTestContainer & SharedVolumes.Images & SipiTestContainer & WhichSipiService,
](
AppConfigForTestContainers.testcontainers,
DspIngestClientLive.layer,
DspIngestTestContainer.layer,
FusekiTestContainer.layer,
SipiTestContainer.layer,
SipiServiceLive.layer,
JwtServiceLive.layer,
SharedVolumes.Images.layer,
StringFormatter.test,
WhichSipiService.live,
)

private val fusekiTestcontainers =
ZLayer.make[FusekiTestContainer & AppConfigurations & JwtService & SipiService & StringFormatter](
ZLayer.make[FusekiTestContainer & AppConfigurations & WhichSipiService](
AppConfigForTestContainers.fusekiOnlyTestcontainer,
FusekiTestContainer.layer,
SipiServiceMock.layer,
JwtServiceLive.layer,
StringFormatter.test,
WhichSipiService.mock,
)

/**
Expand Down

0 comments on commit 73fc75f

Please sign in to comment.