diff --git a/webapi/src/it/scala/org/knora/webapi/responders/admin/ProjectsResponderADMSpec.scala b/webapi/src/it/scala/org/knora/webapi/responders/admin/ProjectsResponderADMSpec.scala index b868ee8d24..a9699ae9bb 100644 --- a/webapi/src/it/scala/org/knora/webapi/responders/admin/ProjectsResponderADMSpec.scala +++ b/webapi/src/it/scala/org/knora/webapi/responders/admin/ProjectsResponderADMSpec.scala @@ -56,53 +56,48 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender { } "return information about a project identified by IRI" in { - appActor ! ProjectGetRequestADM( - identifier = IriIdentifier + appActor ! ProjectGetRequestADM(identifier = + IriIdentifier .fromString(SharedTestDataADM.incunabulaProject.id) - .getOrElseWith(e => throw BadRequestException(e.head.getMessage)), - requestingUser = SharedTestDataADM.rootUser + .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) ) expectMsg(ProjectGetResponseADM(SharedTestDataADM.incunabulaProject)) } "return information about a project identified by shortname" in { - appActor ! ProjectGetRequestADM( - identifier = ShortnameIdentifier + appActor ! ProjectGetRequestADM(identifier = + ShortnameIdentifier .fromString(SharedTestDataADM.incunabulaProject.shortname) - .getOrElseWith(e => throw BadRequestException(e.head.getMessage)), - requestingUser = SharedTestDataADM.rootUser + .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) ) expectMsg(ProjectGetResponseADM(SharedTestDataADM.incunabulaProject)) } "return 'NotFoundException' when the project IRI is unknown" in { - appActor ! ProjectGetRequestADM( - identifier = IriIdentifier + appActor ! ProjectGetRequestADM(identifier = + IriIdentifier .fromString(notExistingProjectButValidProjectIri) - .getOrElseWith(e => throw BadRequestException(e.head.getMessage)), - requestingUser = SharedTestDataADM.rootUser + .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) ) expectMsg(Failure(NotFoundException(s"Project '$notExistingProjectButValidProjectIri' not found"))) } "return 'NotFoundException' when the project shortname is unknown " in { - appActor ! ProjectGetRequestADM( - identifier = ShortnameIdentifier + appActor ! ProjectGetRequestADM(identifier = + ShortnameIdentifier .fromString("wrongshortname") - .getOrElseWith(e => throw BadRequestException(e.head.getMessage)), - requestingUser = SharedTestDataADM.rootUser + .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) ) expectMsg(Failure(NotFoundException(s"Project 'wrongshortname' not found"))) } "return 'NotFoundException' when the project shortcode is unknown " in { - appActor ! ProjectGetRequestADM( - identifier = ShortcodeIdentifier + appActor ! ProjectGetRequestADM(identifier = + ShortcodeIdentifier .fromString("9999") - .getOrElseWith(e => throw BadRequestException(e.head.getMessage)), - requestingUser = SharedTestDataADM.rootUser + .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) ) expectMsg(Failure(NotFoundException(s"Project '9999' not found"))) } diff --git a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala index f026029a70..ab1a282382 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -13,8 +13,9 @@ import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.StringFormatter import org.knora.webapi.responders.ActorDeps import org.knora.webapi.responders.ActorToZioBridge -import org.knora.webapi.responders.admin.ProjectsService +import org.knora.webapi.responders.admin.RestProjectsService import org.knora.webapi.routing.ApiRoutes +import org.knora.webapi.routing.admin.AuthenticatorService import org.knora.webapi.routing.admin.ProjectsRouteZ import org.knora.webapi.slice.resourceinfo.api.ResourceInfoRoute import org.knora.webapi.slice.resourceinfo.api.RestResourceInfoService @@ -63,6 +64,7 @@ object LayersLive { ApiRoutes.layer, AppConfig.live, AppRouter.layer, + AuthenticatorService.layer, CacheServiceInMemImpl.layer, CacheServiceManager.layer, HttpServer.layer, @@ -72,11 +74,11 @@ object LayersLive { IriConverter.layer, JWTService.layer, ProjectsRouteZ.layer, - ProjectsService.layer, RepositoryUpdater.layer, ResourceInfoRepo.layer, ResourceInfoRoute.layer, RestResourceInfoService.layer, + RestProjectsService.layer, State.layer, StringFormatter.live, TriplestoreServiceHttpConnectorImpl.layer, diff --git a/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesZ.scala b/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesZ.scala index 05da7c1095..452adbc332 100644 --- a/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesZ.scala +++ b/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesZ.scala @@ -11,9 +11,8 @@ import dsp.errors._ import org.knora.webapi.store.triplestore.errors.TriplestoreTimeoutException /** - * Migrated from [[org.knora.webapi.http.status.ApiStatusCodes]] - * * The possible values for the HTTP status code that is returned as part of each Knora API v2 response. + * Migrated from [[org.knora.webapi.http.status.ApiStatusCodes]] */ object ApiStatusCodesZ { diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala index 1464713860..457e1b35d2 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala @@ -26,13 +26,12 @@ import org.knora.webapi.IRI import org.knora.webapi.messages.ResponderRequest.KnoraRequestADM import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.KnoraResponseADM +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM._ import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 import org.knora.webapi.messages.store.triplestoremessages.TriplestoreJsonProtocol import org.knora.webapi.messages.v1.responder.projectmessages.ProjectInfoV1 -import ProjectIdentifierADM._ - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // API requests @@ -187,10 +186,7 @@ case class ProjectsGetADM(requestingUser: UserADM) extends ProjectsResponderRequ * @param identifier the IRI, email, or username of the project. * @param requestingUser the user making the request. */ -case class ProjectGetRequestADM( - identifier: ProjectIdentifierADM, - requestingUser: UserADM -) extends ProjectsResponderRequestADM +case class ProjectGetRequestADM(identifier: ProjectIdentifierADM) extends ProjectsResponderRequestADM /** * Get info about a single project identified either through its IRI, shortname or shortcode. The response is in form @@ -198,9 +194,7 @@ case class ProjectGetRequestADM( * * @param identifier the IRI, email, or username of the project. */ -case class ProjectGetADM( - identifier: ProjectIdentifierADM -) extends ProjectsResponderRequestADM +case class ProjectGetADM(identifier: ProjectIdentifierADM) extends ProjectsResponderRequestADM /** * Returns all users belonging to a project identified either through its IRI, shortname or shortcode. diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala index 424ed7edf6..8eca611848 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/ConstructResponseUtilV2.scala @@ -1570,11 +1570,10 @@ object ConstructResponseUtilV2 { projectResponse: ProjectGetResponseADM <- appActor .ask( - ProjectGetRequestADM( - identifier = IriIdentifier + ProjectGetRequestADM(identifier = + IriIdentifier .fromString(resourceAttachedToProject) - .getOrElseWith(e => throw BadRequestException(e.head.getMessage)), - requestingUser = requestingUser + .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) ) ) .mapTo[ProjectGetResponseADM] diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index a6e4a657f8..c0e827a472 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -701,11 +701,10 @@ object CreateResourceRequestV2 extends KnoraJsonLDRequestReaderV2[CreateResource projectInfoResponse: ProjectGetResponseADM <- appActor .ask( - ProjectGetRequestADM( - identifier = IriIdentifier + ProjectGetRequestADM(identifier = + IriIdentifier .fromString(projectIri.toString) - .getOrElseWith(e => throw BadRequestException(e.head.getMessage)), - requestingUser = requestingUser + .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) ) ) .mapTo[ProjectGetResponseADM] diff --git a/webapi/src/main/scala/org/knora/webapi/responders/ActorToZioBridge.scala b/webapi/src/main/scala/org/knora/webapi/responders/ActorToZioBridge.scala index f2107fd9cf..212c8bac51 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/ActorToZioBridge.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/ActorToZioBridge.scala @@ -1,5 +1,4 @@ package org.knora.webapi.responders -import akka.actor.Actor import akka.actor.ActorRef import akka.pattern.ask import akka.util.Timeout @@ -9,8 +8,6 @@ import zio.URLayer import zio.ZIO import zio.ZLayer -import scala.reflect.ClassTag - import org.knora.webapi.messages.ResponderRequest /** @@ -23,14 +20,13 @@ trait ActorToZioBridge { * casts and returns the response to the expected return type `R` as [[Task]]. * * @param message The message sent to the actor - * @param tag implicit proof that the result type `R` has a [[ClassTag]] * * @tparam R The type of the expected success value * @return A Task containing either the success `R` or the failure [[Throwable]], * will fail during runtime with a [[ClassCastException]] if the `R` does not correspond * to the response of the message being sent due to the untyped nature of the ask pattern */ - def askAppActor[R: Tag](message: ResponderRequest)(implicit tag: ClassTag[R]): Task[R] + def askAppActor[R: Tag](message: ResponderRequest): Task[R] } @@ -38,8 +34,8 @@ final case class ActorToZioBridgeLive(actorDeps: ActorDeps) extends ActorToZioBr private implicit val timeout: Timeout = actorDeps.timeout private val appActor: ActorRef = actorDeps.appActor - override def askAppActor[R: Tag](message: ResponderRequest)(implicit tag: ClassTag[R]): Task[R] = - ZIO.fromFuture(_ => appActor.ask(message, Actor.noSender).mapTo[R]) + override def askAppActor[R: Tag](message: ResponderRequest): Task[R] = + ZIO.fromFuture(implicit ec => appActor.ask(message).map(_.asInstanceOf[R])) } object ActorToZioBridge { diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala index 6072d37be9..c242c4eaa6 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala @@ -65,8 +65,7 @@ final case class ProjectsResponderADM(actorDeps: ActorDeps, cacheServiceSettings case ProjectsGetADM(requestingUser) => projectsGetADM(requestingUser) case ProjectsGetRequestADM(requestingUser) => projectsGetRequestADM(requestingUser) case ProjectGetADM(identifier) => getSingleProjectADM(identifier) - case ProjectGetRequestADM(identifier, requestingUser) => - getSingleProjectADMRequest(identifier, requestingUser) + case ProjectGetRequestADM(identifier) => getSingleProjectADMRequest(identifier) case ProjectMembersGetRequestADM(identifier, requestingUser) => projectMembersGetRequestADM(identifier, requestingUser) case ProjectAdminMembersGetRequestADM(identifier, requestingUser) => @@ -251,26 +250,13 @@ final case class ProjectsResponderADM(actorDeps: ActorDeps, cacheServiceSettings * as a [[ProjectGetResponseADM]]. * * @param identifier the IRI, shortname, shortcode or UUID of the project. - * @param requestingUser the user making the request. * @return information about the project as a [[ProjectGetResponseADM]]. * @throws NotFoundException when no project for the given IRI can be found */ - def getSingleProjectADMRequest( - identifier: ProjectIdentifierADM, - requestingUser: UserADM - ): Future[ProjectGetResponseADM] = - for { - maybeProject: Option[ProjectADM] <- getSingleProjectADM( - identifier = identifier - ) - - project = maybeProject match { - case Some(p) => p - case None => throw NotFoundException(s"Project '${getId(identifier)}' not found") - } - } yield ProjectGetResponseADM( - project = project - ) + def getSingleProjectADMRequest(identifier: ProjectIdentifierADM): Future[ProjectGetResponseADM] = for { + maybeProject <- getSingleProjectADM(identifier) + project = maybeProject.getOrElse(throw NotFoundException(s"Project '${getId(identifier)}' not found")) + } yield ProjectGetResponseADM(project) /** * Gets the members of a project with the given IRI, shortname, shortcode or UUID. Returns an empty list diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsService.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsService.scala deleted file mode 100644 index 448747bc97..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsService.scala +++ /dev/null @@ -1,34 +0,0 @@ -package org.knora.webapi.responders.admin -import zio.Task -import zio.URLayer -import zio.ZLayer - -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetRequestADM -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetResponseADM -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM -import org.knora.webapi.messages.admin.responder.usersmessages.UserADM -import org.knora.webapi.responders.ActorToZioBridge - -final case class ProjectsService(bridge: ActorToZioBridge) { - - /** - * Finds the project by its [[ProjectIdentifierADM]] and returns the information as a [[ProjectGetResponseADM]]. - * Checks permissions whether the [[UserADM]] requesting the project may see the result. - * - * @param identifier a [[ProjectIdentifierADM]] instance - * @param requestingUser the user making the request - * @return - * '''success''': information about the project as a [[ProjectGetResponseADM]] - * - * '''error''': [[dsp.errors.NotFoundException]] when no project for the given IRI can be found - */ - def getSingleProjectADMRequest( - identifier: ProjectIdentifierADM, - requestingUser: UserADM - ): Task[ProjectGetResponseADM] = - bridge.askAppActor(ProjectGetRequestADM(identifier, requestingUser)) -} - -object ProjectsService { - val layer: URLayer[ActorToZioBridge, ProjectsService] = ZLayer.fromFunction(ProjectsService.apply _) -} diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/RestProjectsService.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/RestProjectsService.scala new file mode 100644 index 0000000000..073e636b1b --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/RestProjectsService.scala @@ -0,0 +1,45 @@ +package org.knora.webapi.responders.admin +import zio.Task +import zio.URLayer +import zio.ZLayer + +import org.knora.webapi.IRI +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetRequestADM +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetResponseADM +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM +import org.knora.webapi.responders.ActorToZioBridge + +final case class RestProjectsService(bridge: ActorToZioBridge) { + + /** + * Finds the project by an [[IRI]] and returns the information as a [[ProjectGetResponseADM]]. + * + * @param projectIri an [[IRI]] identifying the project + * @return + * '''success''': information about the project as a [[ProjectGetResponseADM]] + * + * '''error''': [[dsp.errors.NotFoundException]] when no project for the given IRI can be found + * [[dsp.errors.ValidationException]] if the given `projectIri` is invalid + */ + def getSingleProjectADMRequest(projectIri: IRI): Task[ProjectGetResponseADM] = + ProjectIdentifierADM.IriIdentifier + .fromString(projectIri) + .toZIO + .flatMap(getSingleProjectADMRequest(_)) + + /** + * Finds the project by its [[ProjectIdentifierADM]] and returns the information as a [[ProjectGetResponseADM]]. + * + * @param identifier a [[ProjectIdentifierADM]] instance + * @return + * '''success''': information about the project as a [[ProjectGetResponseADM]] + * + * '''failure''': [[dsp.errors.NotFoundException]] when no project for the given IRI can be found + */ + def getSingleProjectADMRequest(identifier: ProjectIdentifierADM): Task[ProjectGetResponseADM] = + bridge.askAppActor(ProjectGetRequestADM(identifier)) +} + +object RestProjectsService { + val layer: URLayer[ActorToZioBridge, RestProjectsService] = ZLayer.fromFunction(RestProjectsService.apply _) +} diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v1/ResourcesResponderV1.scala b/webapi/src/main/scala/org/knora/webapi/responders/v1/ResourcesResponderV1.scala index e553f110f7..bed85ed0c6 100755 --- a/webapi/src/main/scala/org/knora/webapi/responders/v1/ResourcesResponderV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v1/ResourcesResponderV1.scala @@ -1584,11 +1584,10 @@ class ResourcesResponderV1(responderData: ResponderData) extends Responder(respo projectInfoResponse <- appActor .ask( - ProjectGetRequestADM( - identifier = IriIdentifier + ProjectGetRequestADM(identifier = + IriIdentifier .fromString(projectIri) - .getOrElseWith(e => throw BadRequestException(e.head.getMessage)), - requestingUser = requestingUser + .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) ) ) .mapTo[ProjectGetResponseADM] @@ -2462,11 +2461,10 @@ class ResourcesResponderV1(responderData: ResponderData) extends Responder(respo projectResponse <- appActor .ask( - ProjectGetRequestADM( - identifier = IriIdentifier + ProjectGetRequestADM(identifier = + IriIdentifier .fromString(projectIri) - .getOrElseWith(e => throw BadRequestException(e.head.getMessage)), - requestingUser = userProfile + .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) ) ) .mapTo[ProjectGetResponseADM] diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v1/ValuesResponderV1.scala b/webapi/src/main/scala/org/knora/webapi/responders/v1/ValuesResponderV1.scala index 1e218cb8df..0c592b327b 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v1/ValuesResponderV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v1/ValuesResponderV1.scala @@ -837,11 +837,10 @@ class ValuesResponderV1(responderData: ResponderData) extends Responder(responde projectResponse <- appActor .ask( - ProjectGetRequestADM( - identifier = IriIdentifier + ProjectGetRequestADM(identifier = + IriIdentifier .fromString(resourceInfoResponse.resource_info.get.project_id) - .getOrElseWith(e => throw BadRequestException(e.head.getMessage)), - requestingUser = changeFileValueRequest.userProfile + .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) ) ) .mapTo[ProjectGetResponseADM] diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/OntologyResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/OntologyResponderV2.scala index a523c60ea3..eedfc3593f 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/OntologyResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/OntologyResponderV2.scala @@ -606,11 +606,10 @@ class OntologyResponderV2(responderData: ResponderData) extends Responder(respon projectInfo: ProjectGetResponseADM <- appActor .ask( - ProjectGetRequestADM( - identifier = IriIdentifier + ProjectGetRequestADM(identifier = + IriIdentifier .fromString(projectIri.toString) - .getOrElseWith(e => throw BadRequestException(e.head.getMessage)), - requestingUser = requestingUser + .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) ) ) .mapTo[ProjectGetResponseADM] diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index 3b527b0ae0..2ad23e6d7d 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -2675,11 +2675,10 @@ class ResourcesResponderV2(responderData: ResponderData) extends ResponderWithSt projectInfoResponse: ProjectGetResponseADM <- appActor .ask( - ProjectGetRequestADM( - identifier = IriIdentifier + ProjectGetRequestADM(identifier = + IriIdentifier .fromString(projectResourceHistoryEventsGetRequest.projectIri) - .getOrElseWith(e => throw BadRequestException(e.head.getMessage)), - requestingUser = projectResourceHistoryEventsGetRequest.requestingUser + .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) ) ) .mapTo[ProjectGetResponseADM] diff --git a/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala b/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala index 6b57b065b6..9537a0ed9a 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala @@ -732,7 +732,7 @@ object Authenticator extends InstrumentationSupport { * @return a [[UserADM]] * @throws AuthenticationException when the IRI can not be found inside the token, which is probably a bug. */ - private def getUserADMThroughCredentialsV2( + def getUserADMThroughCredentialsV2( credentials: Option[KnoraCredentialsV2], appConfig: AppConfig )(implicit diff --git a/webapi/src/main/scala/org/knora/webapi/routing/HealthRouteZ.scala b/webapi/src/main/scala/org/knora/webapi/routing/HealthRouteZ.scala new file mode 100644 index 0000000000..714f0d1149 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/routing/HealthRouteZ.scala @@ -0,0 +1,115 @@ +/* + * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.instrumentation.health + +import spray.json.JsObject +import spray.json.JsString +import zhttp.http._ +import zio._ + +import org.knora.webapi.core.State +import org.knora.webapi.core.domain.AppState + +/** + * Provides the '/health' endpoint serving the health status. + */ +object HealthRouteZ { + + val route: HttpApp[State, Nothing] = + Http.collectZIO[Request] { case Method.GET -> !! / "health" => + State.getAppState.map(toHealthCheckResult).flatMap(createResponse) + } + + /** + * Transforms the [[AppState]] into a [[HealthCheckResult]] + * + * @param state the application's state + * @return the result which is either unhealthy or healthy, containing a human readable explanation in case of unhealthy + */ + private def toHealthCheckResult(state: AppState): HealthCheckResult = + state match { + case AppState.Stopped => unhealthy("Stopped. Please retry later.") + case AppState.StartingUp => unhealthy("Starting up. Please retry later.") + case AppState.WaitingForTriplestore => unhealthy("Waiting for triplestore. Please retry later.") + case AppState.TriplestoreReady => unhealthy("Triplestore ready. Please retry later.") + case AppState.UpdatingRepository => unhealthy("Updating repository. Please retry later.") + case AppState.RepositoryUpToDate => unhealthy("Repository up to date. Please retry later.") + case AppState.CreatingCaches => unhealthy("Creating caches. Please retry later.") + case AppState.CachesReady => unhealthy("Caches ready. Please retry later.") + case AppState.UpdatingSearchIndex => unhealthy("Updating search index. Please retry later.") + case AppState.SearchIndexReady => unhealthy("Search index ready. Please retry later.") + case AppState.LoadingOntologies => unhealthy("Loading ontologies. Please retry later.") + case AppState.OntologiesReady => unhealthy("Ontologies ready. Please retry later.") + case AppState.WaitingForIIIFService => unhealthy("Waiting for IIIF service. Please retry later.") + case AppState.IIIFServiceReady => unhealthy("IIIF service ready. Please retry later.") + case AppState.WaitingForCacheService => unhealthy("Waiting for cache service. Please retry later.") + case AppState.CacheServiceReady => unhealthy("Cache service ready. Please retry later.") + case AppState.MaintenanceMode => unhealthy("Application is in maintenance mode. Please retry later.") + case AppState.Running => healthy + } + + /** + * Creates the HTTP response from the health check result (healthy/unhealthy). + * + * @param result the result of the health check + * @return an HTTP response + */ + private def createResponse(result: HealthCheckResult): UIO[Response] = + ZIO.succeed( + Response + .json( + JsObject( + "name" -> JsString("AppState"), + "severity" -> JsString("non fatal"), + "status" -> JsString(status(result.status)), + "message" -> JsString(result.message) + ).toString() + ) + .setStatus(statusCode(result.status)) + ) + + /** + * Returns a string representation "healthy" or "unhealthy" from a boolean. + * + * @param s a boolean from which to derive the state + * @return either "healthy" or "unhealthy" + */ + private def status(s: Boolean): String = if (s) "healthy" else "unhealthy" + + /** + * Returns the HTTP status according to the input boolean. + * + * @param s a boolean from which to derive the HTTP status + * @return the HTTP status (OK or ServiceUnavailable) + */ + private def statusCode(s: Boolean): Status = if (s) Status.Ok else Status.ServiceUnavailable + + /** + * The result of a health check which is either unhealthy or healthy. + * + * @param name ??? + * @param severity ??? + * @param status the status (either false = unhealthy or true = healthy) + * @param message the message + */ + private case class HealthCheckResult(name: String, severity: String, status: Boolean, message: String) + + private def unhealthy(message: String) = + HealthCheckResult( + name = "AppState", + severity = "non fatal", + status = false, + message = message + ) + + private val healthy = + HealthCheckResult( + name = "AppState", + severity = "non fatal", + status = true, + message = "Application is healthy" + ) +} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilZ.scala b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilZ.scala new file mode 100644 index 0000000000..1fae3dd846 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/routing/RouteUtilZ.scala @@ -0,0 +1,31 @@ +package org.knora.webapi.routing +import zio.Task +import zio.ZIO + +import java.net.URLDecoder + +import dsp.errors.BadRequestException + +object RouteUtilZ { + + /** + * Url decodes a [[String]]. + * Fails if String is not a well formed utf-8 [[String]] in `application/x-www-form-urlencoded` MIME format. + * + * Wraps Java's [[java.net.URLDecoder#decode(java.lang.String, java.lang.String)]] into the zio world. + * + * @param value The value in utf-8 to be url decoded + * @param errorMsg Custom error message for the error type + * @return ```success``` the decoded string + * + * ```failure``` A [[BadRequestException]] with the `errorMsg` + */ + def urlDecode(value: String, errorMsg: String = ""): Task[String] = + ZIO + .attempt(URLDecoder.decode(value, "utf-8")) + .orElseFail( + BadRequestException( + if (!errorMsg.isBlank) errorMsg else s"Not an url encoded utf-8 String '$value'" + ) + ) +} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/AuthenticatorService.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/AuthenticatorService.scala new file mode 100644 index 0000000000..1c76f11e69 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/AuthenticatorService.scala @@ -0,0 +1,15 @@ +package org.knora.webapi.routing.admin + +import zhttp.http._ +import zio.Task +import zio.ZLayer + +import org.knora.webapi.messages.admin.responder.usersmessages._ + +trait AuthenticatorService { + def getUser(request: Request): Task[UserADM] +} + +object AuthenticatorService { + val layer = ZLayer.fromFunction(AuthenticatorServiceLive(_, _, _)) +} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/AuthenticatorServiceLive.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/AuthenticatorServiceLive.scala new file mode 100644 index 0000000000..1d4169dab5 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/AuthenticatorServiceLive.scala @@ -0,0 +1,93 @@ +package org.knora.webapi.routing.admin + +import akka.actor.ActorRef +import akka.actor.ActorSystem +import zhttp.http._ +import zio.Task +import zio.ZIO + +import scala.concurrent.ExecutionContext + +import org.knora.webapi.config.AppConfig +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.messages.admin.responder.usersmessages._ +import org.knora.webapi.messages.v2.routing.authenticationmessages.KnoraCredentialsV2 +import org.knora.webapi.messages.v2.routing.authenticationmessages.KnoraCredentialsV2.KnoraJWTTokenCredentialsV2 +import org.knora.webapi.messages.v2.routing.authenticationmessages.KnoraCredentialsV2.KnoraPasswordCredentialsV2 +import org.knora.webapi.messages.v2.routing.authenticationmessages.KnoraCredentialsV2.KnoraSessionCredentialsV2 +import org.knora.webapi.responders.ActorDeps +import org.knora.webapi.routing.Authenticator +import org.knora.webapi.routing.admin.AuthenticatorServiceLive.extractCredentialsFromRequest + +case class AuthenticatorServiceLive(actorDeps: ActorDeps, appConfig: AppConfig, stringFormatter: StringFormatter) + extends AuthenticatorService { + private implicit val sf: StringFormatter = stringFormatter + private implicit val system: ActorSystem = actorDeps.system + private implicit val appActor: ActorRef = actorDeps.appActor + private implicit val ec: ExecutionContext = actorDeps.executionContext + + private val authCookieName = Authenticator.calculateCookieName(appConfig) + override def getUser(request: Request): Task[UserADM] = + ZIO + .succeed(extractCredentialsFromRequest(request, authCookieName)) + .flatMap(credentials => ZIO.fromFuture(_ => Authenticator.getUserADMThroughCredentialsV2(credentials, appConfig))) +} + +object AuthenticatorServiceLive { + + // visible for testing + def extractCredentialsFromRequest(request: Request, cookieName: String)(implicit + sf: StringFormatter + ): Option[KnoraCredentialsV2] = + extractCredentialsFromParameters(request).orElse(extractCredentialsFromHeader(request, cookieName)) + + private def extractCredentialsFromParameters(request: Request)(implicit + sf: StringFormatter + ): Option[KnoraCredentialsV2] = + extractUserPasswordFromParameters(request).orElse(extractTokenFromParameters(request)) + + private def getFirstValueFromParamKey(key: String, request: Request): Option[String] = { + val url = request.url + val params = url.queryParams + params.get(key).map(_.head) + } + + private def extractUserPasswordFromParameters( + request: Request + )(implicit sf: StringFormatter): Option[KnoraPasswordCredentialsV2] = { + val maybeIri = getFirstValueFromParamKey("iri", request) + val maybeEmail = getFirstValueFromParamKey("email", request) + val maybeUsername = getFirstValueFromParamKey("username", request) + val maybePassword = getFirstValueFromParamKey("password", request) + for { + _ <- List(maybeIri, maybeEmail, maybeUsername).flatten.headOption // given at least one of iri, email or username + password <- maybePassword + identifier = UserIdentifierADM(maybeIri, maybeEmail, maybeUsername) + } yield KnoraPasswordCredentialsV2(identifier, password) + } + + private def extractTokenFromParameters(request: Request): Option[KnoraJWTTokenCredentialsV2] = + getFirstValueFromParamKey("token", request).map(KnoraJWTTokenCredentialsV2) + + private def extractCredentialsFromHeader(request: Request, cookieName: String)(implicit + sf: StringFormatter + ): Option[KnoraCredentialsV2] = + extractBasicAuthEmail(request) + .orElse( + extractBearerToken(request) + .orElse(extractSessionCookie(request, cookieName)) + ) + + private def extractBasicAuthEmail( + request: Request + )(implicit sf: StringFormatter): Option[KnoraPasswordCredentialsV2] = + request.basicAuthorizationCredentials.map(c => + KnoraPasswordCredentialsV2(UserIdentifierADM(maybeEmail = Some(c.uname)), c.upassword) + ) + + private def extractBearerToken(request: Request): Option[KnoraJWTTokenCredentialsV2] = + request.bearerToken.map(KnoraJWTTokenCredentialsV2) + + private def extractSessionCookie(request: Request, cookieName: String) = + request.cookieValue(cookieName).map(c => KnoraSessionCredentialsV2(c.toString)) +} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala index 2346774d89..aa63ac4725 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala @@ -13,6 +13,7 @@ import akka.http.scaladsl.model.headers.`Content-Disposition` import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.PathMatcher import akka.http.scaladsl.server.Route +import akka.http.scaladsl.util.FastFuture import akka.pattern._ import akka.stream.IOResult import akka.stream.scaladsl.FileIO @@ -41,7 +42,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) with Authenticator with ProjectsADMJsonProtocol { - val projectsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "projects") + private val projectsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "projects") /** * Returns the route. @@ -188,21 +189,11 @@ class ProjectsRouteADM(routeData: KnoraRouteData) private def getProjectByIri(): Route = path(projectsBasePath / "iri" / Segment) { value => get { requestContext => - val requestMessage: Future[ProjectGetRequestADM] = for { - requestingUser <- getUserADM( - requestContext = requestContext, - routeData.appConfig - ) - - } yield ProjectGetRequestADM( - identifier = IriIdentifier - .fromString(value) - .getOrElseWith(e => throw BadRequestException(e.head.getMessage)), - requestingUser = requestingUser + val requestMessage = ProjectGetRequestADM( + IriIdentifier.fromString(value).getOrElseWith(e => throw BadRequestException(e.head.getMessage)) ) - RouteUtilADM.runJsonRoute( - requestMessageF = requestMessage, + requestMessageF = FastFuture.successful(requestMessage), requestContext = requestContext, appActor = appActor, log = log @@ -216,18 +207,13 @@ class ProjectsRouteADM(routeData: KnoraRouteData) private def getProjectByShortname(): Route = path(projectsBasePath / "shortname" / Segment) { value => get { requestContext => - val requestMessage: Future[ProjectGetRequestADM] = for { - requestingUser <- getUserADM( - requestContext = requestContext, - routeData.appConfig - ) - - } yield ProjectGetRequestADM( - identifier = ShortnameIdentifier - .fromString(value) - .getOrElseWith(e => throw BadRequestException(e.head.getMessage)), - requestingUser = requestingUser - ) + val requestMessage = Future { + ProjectGetRequestADM(identifier = + ShortnameIdentifier + .fromString(value) + .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) + ) + } RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, @@ -244,18 +230,13 @@ class ProjectsRouteADM(routeData: KnoraRouteData) private def getProjectByShortcode(): Route = path(projectsBasePath / "shortcode" / Segment) { value => get { requestContext => - val requestMessage: Future[ProjectGetRequestADM] = for { - requestingUser <- getUserADM( - requestContext = requestContext, - routeData.appConfig - ) - - } yield ProjectGetRequestADM( - identifier = ShortcodeIdentifier - .fromString(value) - .getOrElseWith(e => throw BadRequestException(e.head.getMessage)), - requestingUser = requestingUser - ) + val requestMessage: Future[ProjectGetRequestADM] = Future { + ProjectGetRequestADM(identifier = + ShortcodeIdentifier + .fromString(value) + .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) + ) + } RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, @@ -613,7 +594,7 @@ class ProjectsRouteADM(routeData: KnoraRouteData) requestingUser = requestingUser ) - responseMessage <- (appActor.ask(requestMessage)).mapTo[ProjectDataGetResponseADM] + responseMessage <- appActor.ask(requestMessage).mapTo[ProjectDataGetResponseADM] // Stream the output file back to the client, then delete the file. diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala index b3fc4314c3..41a3661f99 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala @@ -1,56 +1,40 @@ -/* - * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - package org.knora.webapi.routing.admin import zhttp.http._ import zio.URLayer -import zio.ZIO import zio.ZLayer -import java.net.URLDecoder - -import dsp.errors.BadRequestException import dsp.errors.InternalServerException -import dsp.errors.KnoraException import dsp.errors.RequestRejectedException import org.knora.webapi.config.AppConfig import org.knora.webapi.http.handler.ExceptionHandlerZ -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM -import org.knora.webapi.messages.util.KnoraSystemInstances -import org.knora.webapi.responders.admin.ProjectsService - -final case class ProjectsRouteZ(projectsService: ProjectsService, appConfig: AppConfig) { +import org.knora.webapi.responders.admin.RestProjectsService +import org.knora.webapi.routing.RouteUtilZ - def getProjectByIri(iri: String): ZIO[Any, KnoraException, Response] = - for { - user <- ZIO.succeed(KnoraSystemInstances.Users.SystemUser) - iriDecoded <- - ZIO - .attempt(URLDecoder.decode(iri, "utf-8")) - .orElseFail(BadRequestException(s"Failed to decode IRI $iri")) - iriValue <- ProjectIdentifierADM.IriIdentifier.fromString(iriDecoded).toZIO - response <- projectsService.getSingleProjectADMRequest(iriValue, user).orDie - } yield Response.json(response.toJsValue.toString()) +final case class ProjectsRouteZ( + appConfig: AppConfig, + projectsService: RestProjectsService +) { val route: HttpApp[Any, Nothing] = - (Http - .collectZIO[Request] { - // TODO : Add user authentication, make tests run with the new route - // Returns a single project identified through the IRI. - case Method.GET -> !! / "admin" / "projects" / "iri" / iri => - getProjectByIri(iri) - }) + Http + .collectZIO[Request] { case Method.GET -> !! / "admin" / "projects" / "iri" / iriUrlEncoded => + getProjectByIriEncoded(iriUrlEncoded) + } .catchAll { case RequestRejectedException(e) => ExceptionHandlerZ.exceptionToJsonHttpResponseZ(e, appConfig) case InternalServerException(e) => ExceptionHandlerZ.exceptionToJsonHttpResponseZ(e, appConfig) } + + private def getProjectByIriEncoded(iriUrlEncoded: String) = + RouteUtilZ + .urlDecode(iriUrlEncoded, "Failed to url decode IRI parameter.") + .flatMap(projectsService.getSingleProjectADMRequest(_).map(_.toJsValue.toString())) + .map(Response.json(_)) } object ProjectsRouteZ { - val layer: URLayer[ProjectsService with AppConfig, ProjectsRouteZ] = ZLayer.fromFunction(ProjectsRouteZ.apply _) + val layer: URLayer[AppConfig with RestProjectsService, ProjectsRouteZ] = ZLayer.fromFunction(ProjectsRouteZ.apply _) } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourcesRouteV1.scala b/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourcesRouteV1.scala index 325a138d01..fa6ca0c97c 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourcesRouteV1.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v1/ResourcesRouteV1.scala @@ -314,11 +314,10 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) projectResponse: ProjectGetResponseADM <- appActor .ask( - ProjectGetRequestADM( - identifier = IriIdentifier + ProjectGetRequestADM(identifier = + IriIdentifier .fromString(projectIri) - .getOrElseWith(e => throw BadRequestException(e.head.getMessage)), - requestingUser = userADM + .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) ) ) .mapTo[ProjectGetResponseADM] @@ -449,11 +448,10 @@ class ResourcesRouteV1(routeData: KnoraRouteData) extends KnoraRoute(routeData) projectResponse: ProjectGetResponseADM <- appActor .ask( - ProjectGetRequestADM( - identifier = IriIdentifier + ProjectGetRequestADM(identifier = + IriIdentifier .fromString(projectId) - .getOrElseWith(e => throw BadRequestException(e.head.getMessage)), - requestingUser = userProfile + .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) ) ) .mapTo[ProjectGetResponseADM] diff --git a/webapi/src/test/scala/org/knora/webapi/responders/ActorToZioBridgeMock.scala b/webapi/src/test/scala/org/knora/webapi/responders/ActorToZioBridgeMock.scala index 5a249d0f60..8f7f625c13 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/ActorToZioBridgeMock.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/ActorToZioBridgeMock.scala @@ -8,8 +8,6 @@ import zio.mock import zio.mock.Mock import zio.mock.Proxy -import scala.reflect.ClassTag - import org.knora.webapi.messages.ResponderRequest /** @@ -26,8 +24,7 @@ object ActorToZioBridgeMock extends Mock[ActorToZioBridge] { for { proxy <- ZIO.service[Proxy] } yield new ActorToZioBridge { - override def askAppActor[R: Tag](message: ResponderRequest)(implicit tag: ClassTag[R]): Task[R] = - proxy(AskAppActor.of[R], message) + override def askAppActor[R: Tag](message: ResponderRequest): Task[R] = proxy(AskAppActor.of[R], message) } } } diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/RestProjectsServiceSpec.scala similarity index 58% rename from webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceSpec.scala rename to webapi/src/test/scala/org/knora/webapi/responders/admin/RestProjectsServiceSpec.scala index b9669ed68f..214f46d624 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/RestProjectsServiceSpec.scala @@ -3,28 +3,27 @@ import zio.Scope import zio.ZIO import zio.mock._ import zio.test.Assertion +import zio.test.SmartAssertionOps import zio.test.Spec import zio.test.TestEnvironment import zio.test.ZIOSpecDefault import zio.test.assertTrue +import dsp.errors.ValidationException import dsp.valueobjects.Project.ShortCode import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetRequestADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetResponseADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.ShortcodeIdentifier -import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 -import org.knora.webapi.messages.util.KnoraSystemInstances import org.knora.webapi.responders.ActorToZioBridgeMock -object ProjectsServiceSpec extends ZIOSpecDefault { +object RestProjectsServiceSpec extends ZIOSpecDefault { private val id: ShortcodeIdentifier = ShortcodeIdentifier( ShortCode.make("0001").getOrElse(throw new IllegalArgumentException()) ) - private val user: UserADM = KnoraSystemInstances.Users.SystemUser - private val expectedRequest: ProjectGetRequestADM = ProjectGetRequestADM(id, user) + private val expectedRequest: ProjectGetRequestADM = ProjectGetRequestADM(id) private val expectedResponse: ProjectGetResponseADM = ProjectGetResponseADM( ProjectADM( "id", @@ -40,15 +39,26 @@ object ProjectsServiceSpec extends ZIOSpecDefault { ) ) - private val expectation = ActorToZioBridgeMock.AskAppActor + private val expectSuccess = ActorToZioBridgeMock.AskAppActor .of[ProjectGetResponseADM] .apply(Assertion.equalTo(expectedRequest), Expectation.value(expectedResponse)) + private val expectNoInteraction = ActorToZioBridgeMock.empty + private val systemUnderTest = ZIO.service[RestProjectsService] override def spec: Spec[TestEnvironment with Scope, Any] = - suite("ProjectsService")(test("should send correct message and return expected response") { - for { - sut <- ZIO.service[ProjectsService] - actual <- sut.getSingleProjectADMRequest(id, user) - } yield assertTrue(actual == expectedResponse) - }).provide(ProjectsService.layer, expectation) + suite("RestProjectsService")( + test("should send correct message and return expected response") { + for { + sut <- systemUnderTest + actual <- sut.getSingleProjectADMRequest(id) + } yield assertTrue(actual == expectedResponse) + } + .provide(RestProjectsService.layer, expectSuccess), + test("given an invalid iri should return with a failure") { + for { + sut <- systemUnderTest + result <- sut.getSingleProjectADMRequest("invalid").exit + } yield assertTrue(result.is(_.failure) == ValidationException("Project IRI is invalid.")) + }.provide(RestProjectsService.layer, expectNoInteraction) + ) } diff --git a/webapi/src/test/scala/org/knora/webapi/routing/RouteUtilZSpec.scala b/webapi/src/test/scala/org/knora/webapi/routing/RouteUtilZSpec.scala new file mode 100644 index 0000000000..71fb0ca648 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/routing/RouteUtilZSpec.scala @@ -0,0 +1,34 @@ +package org.knora.webapi.routing + +import zio.test.Assertion._ +import zio.test.ZIOSpecDefault +import zio.test._ +import zio.test.assertTrue + +import dsp.errors.BadRequestException + +object RouteUtilZSpec extends ZIOSpecDefault { + val spec: Spec[Any, Throwable] = suite("routeUtilZSpec")( + suite("function `urlDecode` should")( + test("given a valid encoding, return the decoded value") { + for { + actual <- RouteUtilZ.urlDecode("http%3A%2F%2Frdfh.ch%2Fprojects%2FLw3FC39BSzCwvmdOaTyLqQ") + } yield assertTrue(actual == "http://rdfh.ch/projects/Lw3FC39BSzCwvmdOaTyLqQ") + }, + test("given an empty value should return BadRequestException") { + for { + error <- RouteUtilZ.urlDecode("%-5", "Failed to url decode IRI.").exit + } yield assert(error)( + fails(equalTo(BadRequestException("Failed to url decode IRI."))) + ) + }, + test("given an empty value, return BadRequestException with default error message") { + for { + error <- RouteUtilZ.urlDecode("%-5").exit + } yield assert(error)( + fails(equalTo(BadRequestException("Not an url encoded utf-8 String '%-5'"))) + ) + } + ) + ) +} diff --git a/webapi/src/test/scala/org/knora/webapi/routing/admin/AuthenticatorServiceLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/routing/admin/AuthenticatorServiceLiveSpec.scala new file mode 100644 index 0000000000..cbc0dcdbe2 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/routing/admin/AuthenticatorServiceLiveSpec.scala @@ -0,0 +1,87 @@ +package org.knora.webapi.routing.admin +import zhttp.http.Cookie +import zhttp.http.Headers +import zhttp.http.Request +import zhttp.http.URL +import zio.test._ + +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.messages.admin.responder.usersmessages.UserIdentifierADM +import org.knora.webapi.messages.v2.routing.authenticationmessages.KnoraCredentialsV2.KnoraJWTTokenCredentialsV2 +import org.knora.webapi.messages.v2.routing.authenticationmessages.KnoraCredentialsV2.KnoraPasswordCredentialsV2 +import org.knora.webapi.messages.v2.routing.authenticationmessages.KnoraCredentialsV2.KnoraSessionCredentialsV2 + +object AuthenticatorServiceLiveSpec extends ZIOSpecDefault { + + private val cookieName = "cookieName" + + private implicit val sf: StringFormatter = { StringFormatter.initForTest(); StringFormatter.getGeneralInstance } + + val spec = suite("AuthenticatorServiceLiveSpec")( + suite("given header authentication")( + test("should extract user email (basic auth)") { + val userMail: String = "user@example.com" + val req = Request().setHeaders(Headers.basicAuthorizationHeader(userMail, "pass")) + val actual = AuthenticatorServiceLive.extractCredentialsFromRequest(req, cookieName) + val expected = Some(KnoraPasswordCredentialsV2(UserIdentifierADM(maybeEmail = Some(userMail)), "pass")) + assertTrue(actual == expected) + }, + test("should extract jwt token (bearer token)") { + val jwtToken = "someToken" + val req = Request().setHeaders(Headers.bearerAuthorizationHeader(jwtToken)) + val actual = AuthenticatorServiceLive.extractCredentialsFromRequest(req, cookieName) + val expected = Some(KnoraJWTTokenCredentialsV2(jwtToken)) + assertTrue(actual == expected) + }, + test("should extract session cookie") { + val sessionCookieValue = "session" + val req = Request().setHeaders(Headers.cookie(Cookie(cookieName, sessionCookieValue))) + val actual = AuthenticatorServiceLive.extractCredentialsFromRequest(req, cookieName) + val expected = Some(KnoraSessionCredentialsV2(sessionCookieValue)) + assertTrue(actual == expected) + } + ), + suite("given query parameters authentication")( + test("should extract jwt token") { + val jwtToken = "someToken" + val req = Request().setUrl(URL.empty.setQueryParams(Map("token" -> List(jwtToken)))) + val actual = AuthenticatorServiceLive.extractCredentialsFromRequest(req, cookieName) + val expected = Some(KnoraJWTTokenCredentialsV2(jwtToken)) + assertTrue(actual == expected) + }, + test("should extract username and password") { + val username = "someUsername" + val password = "somePassword" + val req = + Request().setUrl(URL.empty.setQueryParams(Map("username" -> List(username), "password" -> List(password)))) + val actual = AuthenticatorServiceLive.extractCredentialsFromRequest(req, cookieName) + val expected = Some(KnoraPasswordCredentialsV2(UserIdentifierADM(maybeUsername = Some(username)), password)) + assertTrue(actual == expected) + }, + test("should extract email and password") { + val email = "user@example.com" + val password = "somePassword" + val req = + Request().setUrl(URL.empty.setQueryParams(Map("email" -> List(email), "password" -> List(password)))) + val actual = AuthenticatorServiceLive.extractCredentialsFromRequest(req, cookieName) + val expected = Some(KnoraPasswordCredentialsV2(UserIdentifierADM(maybeEmail = Some(email)), password)) + assertTrue(actual == expected) + }, + test("should extract iri and password") { + val userIri = "http://rdfh.ch/users/someUser" + val password = "somePassword" + val req = + Request().setUrl(URL.empty.setQueryParams(Map("iri" -> List(userIri), "password" -> List(password)))) + val actual = AuthenticatorServiceLive.extractCredentialsFromRequest(req, cookieName) + val expected = Some(KnoraPasswordCredentialsV2(UserIdentifierADM(maybeIri = Some(userIri)), password)) + assertTrue(actual == expected) + } + ), + suite("when nothing is given")( + test("should return None") { + val actual = AuthenticatorServiceLive.extractCredentialsFromRequest(Request(), cookieName) + assertTrue(actual.isEmpty) + } + ) + ) +} diff --git a/webapi/src/test/scala/org/knora/webapi/routing/admin/ProjectRouteZSpec.scala b/webapi/src/test/scala/org/knora/webapi/routing/admin/ProjectRouteZSpec.scala new file mode 100644 index 0000000000..b92508324f --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/routing/admin/ProjectRouteZSpec.scala @@ -0,0 +1,77 @@ +package org.knora.webapi.routing.admin + +import zhttp.http._ +import zio._ +import zio.mock.Expectation +import zio.test.ZIOSpecDefault +import zio.test._ + +import java.net.URLEncoder + +import org.knora.webapi.config.AppConfig +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetRequestADM +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetResponseADM +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM +import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 +import org.knora.webapi.responders.ActorToZioBridge +import org.knora.webapi.responders.ActorToZioBridgeMock +import org.knora.webapi.responders.admin.RestProjectsService + +object ProjectRouteZSpec extends ZIOSpecDefault { + + private val systemUnderTest: URIO[ProjectsRouteZ, HttpApp[Any, Nothing]] = ZIO.service[ProjectsRouteZ].map(_.route) + + // Test data + private val projectIri: ProjectIdentifierADM.IriIdentifier = + ProjectIdentifierADM.IriIdentifier + .fromString("http://rdfh.ch/projects/0001") + .getOrElse(throw new IllegalArgumentException()) + + private val basePath: Path = !! / "admin" / "projects" / "iri" + private val validIriEncoded: String = URLEncoder.encode(projectIri.value.value, "utf-8") + private val expectedRequestSuccess: ProjectGetRequestADM = ProjectGetRequestADM(projectIri) + private val expectedResponseSuccess: ProjectGetResponseADM = ProjectGetResponseADM( + ProjectADM( + "id", + "shortname", + "shortcode", + None, + List(StringLiteralV2("description")), + List.empty, + None, + List.empty, + status = false, + selfjoin = false + ) + ) + + // Expectations and layers for ActorToZioBridge mock + private val commonLayers: URLayer[ActorToZioBridge, ProjectsRouteZ] = + ZLayer.makeSome[ActorToZioBridge, ProjectsRouteZ](AppConfig.test, ProjectsRouteZ.layer, RestProjectsService.layer) + + private val expectMessageToProjectResponderADM: ULayer[ActorToZioBridge] = + ActorToZioBridgeMock.AskAppActor + .of[ProjectGetResponseADM] + .apply(Assertion.equalTo(expectedRequestSuccess), Expectation.value(expectedResponseSuccess)) + .toLayer + private val expectNoInteractionWithProjectsResponderADM = ActorToZioBridgeMock.empty + + val spec: Spec[Any, Serializable] = + suite("ProjectsRouteZSpec")( + test("given valid project iri should respond with success") { + val urlWithValidIri = URL.empty.setPath(basePath / validIriEncoded) + for { + route <- systemUnderTest + actual <- route.apply(Request(url = urlWithValidIri)).flatMap(_.body.asString) + } yield assertTrue(actual == expectedResponseSuccess.toJsValue.toString()) + }.provide(commonLayers, expectMessageToProjectResponderADM), + test("given invalid project iri but no authentication should respond with bad request") { + val urlWithInvalidIri = URL.empty.setPath(basePath / "invalid") + for { + route <- systemUnderTest + actual <- route.apply(Request(url = urlWithInvalidIri)).map(_.status) + } yield assertTrue(actual == Status.BadRequest) + }.provide(commonLayers, expectNoInteractionWithProjectsResponderADM) + ) +}