From e1b6eebea45344a4486a557137c82b23f9acfe3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 19 Oct 2023 14:04:48 +0200 Subject: [PATCH] refactor!: Remove zio admin/projects http routes on port 5555 (DEV-2809) (#2879) --- .../test/scala/org/knora/sipi/SipiIT.scala | 66 +- .../test/scala/org/knora/webapi/E2ESpec.scala | 8 +- .../org/knora/webapi/core/LayersTest.scala | 6 - .../e2e/admin/ProjectsADME2EZioHttpSpec.scala | 2 +- .../testcontainers/FusekiTestContainer.scala | 4 +- .../testcontainers/SipiTestContainer.scala | 54 +- project/Dependencies.scala | 7 +- .../org/knora/webapi/core/HttpServerZ.scala | 67 +- .../webapi/core/InstrumentationServer.scala | 4 +- .../org/knora/webapi/core/LayersLive.scala | 6 - .../http/handler/ExceptionHandlerZ.scala | 6 +- .../middleware/AuthenticationMiddleware.scala | 54 -- .../webapi/http/status/ApiStatusCodesZ.scala | 2 +- .../instrumentation/health/HealthRouteZ.scala | 5 +- .../instrumentation/index/IndexApp.scala | 3 +- .../prometheus/PrometheusApp.scala | 6 +- .../routing/admin/AuthenticatorService.scala | 29 - .../admin/AuthenticatorServiceLive.scala | 90 --- .../webapi/routing/admin/ProjectsRouteZ.scala | 273 ------- .../domain/service/DspIngestClient.scala | 14 +- .../domain/service/ProjectImportService.scala | 4 +- .../resourceinfo/api/ResourceInfoRoute.scala | 11 +- .../api/RestResourceInfoService.scala | 2 +- .../api/RestResourceInfoServiceLive.scala | 2 +- .../AuthenticationMiddlewareSpec.scala | 84 -- .../admin/AuthenticatorServiceLiveSpec.scala | 169 ---- .../routing/admin/ProjectsRouteZSpec.scala | 758 ------------------ .../api/LiveRestResourceInfoServiceSpec.scala | 2 +- .../api/ResourceInfoRouteSpec.scala | 19 +- .../api/RestResourceInfoServiceSpy.scala | 4 +- 30 files changed, 132 insertions(+), 1629 deletions(-) delete mode 100644 webapi/src/main/scala/org/knora/webapi/http/middleware/AuthenticationMiddleware.scala delete mode 100644 webapi/src/main/scala/org/knora/webapi/routing/admin/AuthenticatorService.scala delete mode 100644 webapi/src/main/scala/org/knora/webapi/routing/admin/AuthenticatorServiceLive.scala delete mode 100644 webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala delete mode 100644 webapi/src/test/scala/org/knora/webapi/http/middleware/AuthenticationMiddlewareSpec.scala delete mode 100644 webapi/src/test/scala/org/knora/webapi/routing/admin/AuthenticatorServiceLiveSpec.scala delete mode 100644 webapi/src/test/scala/org/knora/webapi/routing/admin/ProjectsRouteZSpec.scala diff --git a/integration/src/test/scala/org/knora/sipi/SipiIT.scala b/integration/src/test/scala/org/knora/sipi/SipiIT.scala index b303073144..0ef26f8a85 100644 --- a/integration/src/test/scala/org/knora/sipi/SipiIT.scala +++ b/integration/src/test/scala/org/knora/sipi/SipiIT.scala @@ -13,7 +13,6 @@ import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder.newRequestPattern import zio._ import zio.http._ -import zio.http.model.Status import zio.json.DecoderOps import zio.json.ast.Json import zio.test._ @@ -41,8 +40,12 @@ object SipiIT extends ZIOSpecDefault { SipiTestContainer.copyFileToImageFolderInContainer(prefix, _) ) - private def getWithoutAuthorization(path: String) = - SipiTestContainer.resolveUrl(path).map(Request.get).flatMap(Client.request(_)) + private def getWithoutAuthorization(path: Path) = + SipiTestContainer + .resolveUrl(path) + .tap(url => Console.printLine(s"SIPI URL resolved: GET $url")) + .map(Request.get) + .flatMap(Client.request(_)) private val getToken = JwtService .createJwt(SystemUser) @@ -63,12 +66,22 @@ object SipiIT extends ZIOSpecDefault { _ <- MockDspApiServer.resetAndAllowWithPermissionCode(prefix, imageTestfile, 2) response <- SipiTestContainer - .resolveUrl(s"/$prefix/$imageTestfile/file") + .resolveUrl(Root / prefix / imageTestfile / "file") .map { url => Request .get(url) - .withCookie( - s"KnoraAuthenticationGAXDALRQFYYDUMZTGMZQ9999aSecondCookie=anotherValueShouldBeIgnored; KnoraAuthenticationGAXDALRQFYYDUMZTGMZQ9999=$jwt" + .addHeaders( + Headers( + Header.Cookie( + NonEmptyChunk( + Cookie.Request( + s"KnoraAuthenticationGAXDALRQFYYDUMZTGMZQ9999aSecondCookie", + "anotherValueShouldBeIgnored" + ), + Cookie.Request("KnoraAuthenticationGAXDALRQFYYDUMZTGMZQ9999", jwt) + ) + ) + ) ) } .flatMap(Client.request(_)) @@ -87,8 +100,16 @@ object SipiIT extends ZIOSpecDefault { _ <- MockDspApiServer.resetAndAllowWithPermissionCode(prefix, imageTestfile, 2) response <- SipiTestContainer - .resolveUrl(s"/$prefix/$imageTestfile/file") - .map(url => Request.get(url).withCookie(s"KnoraAuthenticationGAXDALRQFYYDUMZTGMZQ9999=$jwt")) + .resolveUrl(Root / prefix / imageTestfile / "file") + .map(url => + Request + .get(url) + .addHeaders( + Headers( + Header.Cookie(NonEmptyChunk(Cookie.Request("KnoraAuthenticationGAXDALRQFYYDUMZTGMZQ9999", jwt))) + ) + ) + ) .flatMap(Client.request(_)) requestToDspApiContainsJwt <- MockDspApiServer.verifyAuthBearerTokenReceived(jwt) } yield assertTrue(response.status == Status.Ok, requestToDspApiContainsJwt) @@ -104,10 +125,10 @@ object SipiIT extends ZIOSpecDefault { "when getting the file, " + "then Sipi responds with Ok" ) { - def expectedJson(port: Int) = + def expectedJson(port: Int, host: String) = s"""{ | "@context":"http://sipi.io/api/file/3/context.json", - | "id":"http://localhost:$port/0001/FGiLaT4zzuV-CqwbEDFAFeS.jp2", + | "id":"http://$host:$port/0001/FGiLaT4zzuV-CqwbEDFAFeS.jp2", | "checksumOriginal":"fb252a4fb3d90ce4ebc7e123d54a4112398a7994541b11aab5e4230eac01a61c", | "checksumDerivative":"0ce405c9b183fb0d0a9998e9a49e39c93b699e0f8e2a9ac3496c349e5cea09cc", | "width":250, @@ -119,9 +140,9 @@ object SipiIT extends ZIOSpecDefault { for { _ <- MockDspApiServer.resetAndAllowWithPermissionCode(prefix, imageTestfile, permissionCode = 2) _ <- copyTestFilesToSipi - response <- getWithoutAuthorization(s"/$prefix/$imageTestfile/knora.json") + response <- getWithoutAuthorization(Root / prefix / imageTestfile / "knora.json") json <- response.body.asString.map(_.fromJson[Json]) - expected <- SipiTestContainer.port.map(expectedJson) + expected <- SipiTestContainer.portAndHost.map { case (port, host) => expectedJson(port, host) } } yield assertTrue( response.status == Status.Ok, json == expected @@ -138,7 +159,7 @@ object SipiIT extends ZIOSpecDefault { test("When getting the file, then Sipi responds with Not Found") { for { server <- MockDspApiServer.resetAndGetWireMockServer - response <- getWithoutAuthorization(s"/$prefix/doesnotexist.jp2/file") + response <- getWithoutAuthorization(Root / prefix / "doesnotexist.jp2" / "file") } yield assertTrue(response.status == Status.NotFound, verifyNoInteractionWith(server)) } ), @@ -153,7 +174,7 @@ object SipiIT extends ZIOSpecDefault { for { server <- MockDspApiServer.resetAndStubGetResponse(dspApiPermissionPath, 200, dspApiResponse) _ <- copyTestFilesToSipi - response <- getWithoutAuthorization(s"/$prefix/$imageTestfile/file") + response <- getWithoutAuthorization(Root / prefix / imageTestfile / "file") } yield assertTrue( response.status == Status.Ok, verifySingleGetRequest(server, dspApiPermissionPath) @@ -169,7 +190,7 @@ object SipiIT extends ZIOSpecDefault { for { server <- MockDspApiServer.resetAndStubGetResponse(dspApiPermissionPath, 200, dspApiResponse) _ <- copyTestFilesToSipi - response <- getWithoutAuthorization(s"/$prefix/$imageTestfile/file") + response <- getWithoutAuthorization(Root / prefix / imageTestfile / "file") } yield assertTrue( response.status == Status.Unauthorized, verifySingleGetRequest(server, dspApiPermissionPath) @@ -184,7 +205,7 @@ object SipiIT extends ZIOSpecDefault { for { server <- MockDspApiServer.resetAndStubGetResponse(dspApiPermissionPath, 404) _ <- copyTestFilesToSipi - response <- getWithoutAuthorization(s"/$prefix/$imageTestfile/file") + response <- getWithoutAuthorization(Root / prefix / imageTestfile / "file") } yield assertTrue( response.status == Status.NotFound, verifySingleGetRequest(server, dspApiPermissionPath) @@ -202,8 +223,9 @@ object SipiIT extends ZIOSpecDefault { "then Sipi responds with Not Found" ) { for { - server <- MockDspApiServer.resetAndGetWireMockServer - response <- getWithoutAuthorization(s"/$prefix/doesnotexist.jp2/full/max/0/default.jp2") + server <- MockDspApiServer.resetAndGetWireMockServer + response <- + getWithoutAuthorization(Root / prefix / "doesnotexist.jp2" / "full" / "max" / "0" / "default.jp2") } yield assertTrue(response.status == Status.NotFound, verifyNoInteractionWith(server)) } ), @@ -218,7 +240,7 @@ object SipiIT extends ZIOSpecDefault { for { server <- MockDspApiServer.resetAndStubGetResponse(dspApiPermissionPath, 200, dspApiResponse) _ <- copyTestFilesToSipi - response <- getWithoutAuthorization(s"/$prefix/$imageTestfile/full/max/0/default.jp2") + response <- getWithoutAuthorization(Root / prefix / imageTestfile / "full/max/0/default.jp2") } yield assertTrue( response.status == Status.Ok, verifySingleGetRequest(server, dspApiPermissionPath) @@ -234,7 +256,7 @@ object SipiIT extends ZIOSpecDefault { for { server <- MockDspApiServer.resetAndStubGetResponse(dspApiPermissionPath, 200, dspApiResponse) _ <- copyTestFilesToSipi - response <- getWithoutAuthorization(s"/$prefix/$imageTestfile/full/max/0/default.jp2") + response <- getWithoutAuthorization(Root / prefix / imageTestfile / "full/max/0/default.jp2") } yield assertTrue( response.status == Status.Unauthorized, verifySingleGetRequest(server, dspApiPermissionPath) @@ -249,7 +271,7 @@ object SipiIT extends ZIOSpecDefault { for { server <- MockDspApiServer.resetAndStubGetResponse(dspApiPermissionPath, 404) _ <- copyTestFilesToSipi - response <- getWithoutAuthorization(s"/$prefix/$imageTestfile/full/max/0/default.jp2") + response <- getWithoutAuthorization(Root / prefix / imageTestfile / "full/max/0/default.jp2") } yield assertTrue( response.status == Status.NotFound, verifySingleGetRequest(server, dspApiPermissionPath) @@ -267,7 +289,7 @@ object SipiIT extends ZIOSpecDefault { test("health check works") { for { server <- MockDspApiServer.resetAndGetWireMockServer - response <- getWithoutAuthorization("/server/test.html") + response <- getWithoutAuthorization(Root / "server" / "test.html") } yield assertTrue(response.status.isSuccess, verifyNoInteractionWith(server)) } ) diff --git a/integration/src/test/scala/org/knora/webapi/E2ESpec.scala b/integration/src/test/scala/org/knora/webapi/E2ESpec.scala index 0f0e64bf18..29cd792728 100644 --- a/integration/src/test/scala/org/knora/webapi/E2ESpec.scala +++ b/integration/src/test/scala/org/knora/webapi/E2ESpec.scala @@ -105,11 +105,9 @@ abstract class E2ESpec val appActor = router.ref // needed by some tests - val appConfig = config - val routeData = KnoraRouteData(system, appActor, appConfig) - val baseApiUrl = - if (sys.props.get("key") == Some("zio")) "http://0.0.0.0:5555" - else appConfig.knoraApi.internalKnoraApiBaseUrl + val appConfig = config + val routeData = KnoraRouteData(system, appActor, appConfig) + val baseApiUrl = appConfig.knoraApi.internalKnoraApiBaseUrl final override def beforeAll(): Unit = /* Here we start our app and initialize the repository before each suit runs */ diff --git a/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala b/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala index 579154f748..c3f610fd38 100644 --- a/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala +++ b/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala @@ -10,7 +10,6 @@ import zio._ import org.knora.webapi.config.AppConfig.AppConfigurations import org.knora.webapi.config.AppConfigForTestContainers -import org.knora.webapi.http.middleware.AuthenticationMiddleware import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.util._ import org.knora.webapi.messages.util.search.QueryTraverser @@ -28,8 +27,6 @@ import org.knora.webapi.responders.v2.ontology.CardinalityHandlerLive import org.knora.webapi.responders.v2.ontology.OntologyHelpers import org.knora.webapi.responders.v2.ontology.OntologyHelpersLive import org.knora.webapi.routing._ -import org.knora.webapi.routing.admin.AuthenticatorService -import org.knora.webapi.routing.admin.ProjectsRouteZ import org.knora.webapi.slice.admin.api._ import org.knora.webapi.slice.admin.api.service.MaintenanceRestService import org.knora.webapi.slice.admin.api.service.ProjectADMRestService @@ -135,9 +132,7 @@ object LayersTest { AdminApiRoutes.layer, ApiRoutes.layer, AppRouter.layer, - AuthenticationMiddleware.layer, AuthenticatorLive.layer, - AuthenticatorService.layer, BaseEndpoints.layer, CacheServiceInMemImpl.layer, CacheServiceRequestMessageHandlerLive.layer, @@ -180,7 +175,6 @@ object LayersTest { ProjectsEndpoints.layer, ProjectsEndpointsHandler.layer, ProjectsResponderADMLive.layer, - ProjectsRouteZ.layer, QueryTraverser.layer, RepositoryUpdater.layer, ResourceInfoRepo.layer, diff --git a/integration/src/test/scala/org/knora/webapi/e2e/admin/ProjectsADME2EZioHttpSpec.scala b/integration/src/test/scala/org/knora/webapi/e2e/admin/ProjectsADME2EZioHttpSpec.scala index 524049f977..650a6ea53e 100644 --- a/integration/src/test/scala/org/knora/webapi/e2e/admin/ProjectsADME2EZioHttpSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/e2e/admin/ProjectsADME2EZioHttpSpec.scala @@ -60,7 +60,7 @@ class ProjectsADME2EZioHttpSpec extends E2ESpec with ProjectsADMJsonProtocol { ) ) - s"The Projects Route ($baseApiUrl -> 'admin/projects')" when { + s"The Projects Route 'admin/projects'" when { "used to query for project information" should { "return all projects excluding built-in system projects" in { val request = Get(baseApiUrl + s"/admin/projects") ~> addCredentials(BasicHttpCredentials(rootEmail, testPass)) diff --git a/integration/src/test/scala/org/knora/webapi/testcontainers/FusekiTestContainer.scala b/integration/src/test/scala/org/knora/webapi/testcontainers/FusekiTestContainer.scala index 97dcd26ca1..6a96bc55f1 100644 --- a/integration/src/test/scala/org/knora/webapi/testcontainers/FusekiTestContainer.scala +++ b/integration/src/test/scala/org/knora/webapi/testcontainers/FusekiTestContainer.scala @@ -28,7 +28,7 @@ trait FusekiTestContainer extends GenericContainer[FusekiTestContainer] { def baseUrl: URL = { val urlString = s"http://$getHost:$getFirstMappedPort" - URL.fromString(urlString).getOrElse(throw new IllegalStateException(s"Invalid URL $urlString")) + URL.decode(urlString).getOrElse(throw new IllegalStateException(s"Invalid URL $urlString")) } def credentials: (String, String) = ("admin", FusekiTestContainer.adminPassword) @@ -48,7 +48,7 @@ trait FusekiTestContainer extends GenericContainer[FusekiTestContainer] { .map(_.map(line => line.replace("@REPOSITORY@", repositoryName)).mkString("\n")) request = HttpRequest .newBuilder() - .uri(baseUrl.setPath("/$/datasets").toJavaURI) + .uri(baseUrl.withPath("/$/datasets").toJavaURI) .POST(BodyPublishers.ofString(fusekiConfig)) .header("Content-Type", "text/turtle; charset=utf-8") .build() diff --git a/integration/src/test/scala/org/knora/webapi/testcontainers/SipiTestContainer.scala b/integration/src/test/scala/org/knora/webapi/testcontainers/SipiTestContainer.scala index f9bb9bd007..84a3468b4d 100644 --- a/integration/src/test/scala/org/knora/webapi/testcontainers/SipiTestContainer.scala +++ b/integration/src/test/scala/org/knora/webapi/testcontainers/SipiTestContainer.scala @@ -9,14 +9,18 @@ import org.testcontainers.containers.BindMode import org.testcontainers.containers.GenericContainer import org.testcontainers.utility.DockerImageName import org.testcontainers.utility.MountableFile -import zio._ +import zio.Task +import zio.UIO +import zio.URIO +import zio.ZIO +import zio.ZLayer +import zio.http import zio.http.URL import zio.nio.file.Path -import java.net.NetworkInterface -import java.net.UnknownHostException +import java.net.Inet6Address +import java.net.InetAddress import java.nio.file.Paths -import scala.jdk.CollectionConverters._ import org.knora.webapi.http.version.BuildInfo @@ -36,19 +40,36 @@ final case class SipiTestContainer(container: GenericContainer[Nothing]) { ) } - def host: String = container.getHost - def port: Int = container.getFirstMappedPort - def sipiBaseUrl: URL = { + val port: Int = container.getFirstMappedPort + + val host: String = SipiTestContainer.localHostAddress + + val sipiBaseUrl: URL = { val urlString = s"http://$host:$port" - URL.fromString(urlString).getOrElse(throw new IllegalStateException(s"Invalid URL $urlString")) + println(s"SIPI URL String: $urlString") + val url = URL.decode(urlString).getOrElse(throw new IllegalStateException(s"Invalid URL $urlString")) + println(s"SIPI URL: $url") + url } } object SipiTestContainer { - def port: ZIO[SipiTestContainer, Nothing, Int] = ZIO.serviceWith[SipiTestContainer](_.port) - def resolveUrl(path: String): URIO[SipiTestContainer, URL] = - ZIO.serviceWith[SipiTestContainer](_.sipiBaseUrl.setPath(path)) + val localHostAddress: String = { + val localhost = InetAddress.getLocalHost + if (localhost.isInstanceOf[Inet6Address]) { + s"[${localhost.getHostAddress}]" + } else { + localhost.getHostAddress + } + } + + def port: ZIO[SipiTestContainer, Nothing, Int] = ZIO.serviceWith[SipiTestContainer](_.port) + def host: ZIO[SipiTestContainer, Nothing, String] = ZIO.serviceWith[SipiTestContainer](_.host) + def portAndHost: ZIO[SipiTestContainer, Nothing, (Int, String)] = port <*> host + + def resolveUrl(path: http.Path): URIO[SipiTestContainer, URL] = + ZIO.serviceWith[SipiTestContainer](_.sipiBaseUrl.withPath(path)) def copyFileToImageFolderInContainer(prefix: String, filename: String): ZIO[SipiTestContainer, Throwable, Unit] = ZIO.serviceWithZIO[SipiTestContainer](_.copyFileToImageFolderInContainer(prefix, filename)) @@ -60,13 +81,8 @@ object SipiTestContainer { * A functional effect that initiates a Sipi Testcontainer */ val acquire: UIO[GenericContainer[Nothing]] = ZIO.attemptBlocking { - // get local IP address, which we need for SIPI - val localIpAddress: String = NetworkInterface.getNetworkInterfaces.asScala.toSeq - .filter(!_.isLoopback) - .flatMap(_.getInetAddresses.asScala.toSeq.filter(_.getAddress.length == 4).map(_.toString)) - .headOption - .getOrElse(throw new UnknownHostException("No suitable network interface found")) - + // Uncomment the following line to use the latest version of Sipi for local development: + // val sipiImageName: DockerImageName = DockerImageName.parse(s"daschswiss/knora-sipi:latest") val sipiImageName: DockerImageName = DockerImageName.parse(s"daschswiss/knora-sipi:${BuildInfo.version}") val sipiContainer = new GenericContainer(sipiImageName) sipiContainer.withExposedPorts(1024) @@ -75,7 +91,7 @@ object SipiTestContainer { sipiContainer.withEnv("SIPI_EXTERNAL_PROTOCOL", "http") sipiContainer.withEnv("SIPI_EXTERNAL_HOSTNAME", "0.0.0.0") sipiContainer.withEnv("SIPI_EXTERNAL_PORT", "1024") - sipiContainer.withEnv("SIPI_WEBAPI_HOSTNAME", localIpAddress) + sipiContainer.withEnv("SIPI_WEBAPI_HOSTNAME", SipiTestContainer.localHostAddress) sipiContainer.withEnv("SIPI_WEBAPI_PORT", "3333") sipiContainer.withEnv("CLEAN_TMP_DIR_USER", "clean_tmp_dir_user") sipiContainer.withEnv("CLEAN_TMP_DIR_PW", "clean_tmp_dir_pw") diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 943f0788bb..c6fddf102c 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -22,8 +22,6 @@ object Dependencies { val JenaVersion = "4.8.0" val ZioConfigVersion = "3.0.7" - val ZioHttpVersionOld = "2.0.0-RC11" - val ZioHttpVersion = "0.0.3" val ZioLoggingVersion = "2.1.14" val ZioNioVersion = "2.0.2" val ZioMetricsConnectorsVersion = "2.2.0" @@ -36,8 +34,6 @@ object Dependencies { val zioConfig = "dev.zio" %% "zio-config" % ZioConfigVersion val zioConfigMagnolia = "dev.zio" %% "zio-config-magnolia" % ZioConfigVersion val zioConfigTypesafe = "dev.zio" %% "zio-config-typesafe" % ZioConfigVersion - val zioHttpOld = "io.d11" %% "zhttp" % ZioHttpVersionOld - val zioHttp = "dev.zio" %% "zio-http" % ZioHttpVersion val zioJson = "dev.zio" %% "zio-json" % "0.6.2" val zioLogging = "dev.zio" %% "zio-logging" % ZioLoggingVersion val zioLoggingSlf4jBridge = "dev.zio" %% "zio-logging-slf4j2-bridge" % ZioLoggingVersion @@ -129,7 +125,7 @@ object Dependencies { val tapir = Seq( "com.softwaremill.sttp.tapir" %% "tapir-pekko-http-server" % tapirVersion, -// "com.softwaremill.sttp.tapir" %% "tapir-zio-http-server" % tapirVersion, + "com.softwaremill.sttp.tapir" %% "tapir-zio-http-server" % tapirVersion, "com.softwaremill.sttp.tapir" %% "tapir-json-zio" % tapirVersion, "com.softwaremill.sttp.tapir" %% "tapir-json-spray" % tapirVersion, "com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % tapirVersion, @@ -189,7 +185,6 @@ object Dependencies { zioConfig, zioConfigMagnolia, zioConfigTypesafe, - zioHttp, zioJson, zioLogging, zioLoggingSlf4jBridge, diff --git a/webapi/src/main/scala/org/knora/webapi/core/HttpServerZ.scala b/webapi/src/main/scala/org/knora/webapi/core/HttpServerZ.scala index f9e89bf38d..95cc21df30 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/HttpServerZ.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/HttpServerZ.scala @@ -7,75 +7,22 @@ package org.knora.webapi.core import zio._ import zio.http._ -import zio.http.middleware.Cors -import zio.http.model.Method -import zio.logging.LogAnnotation.TraceId - -import java.util.UUID -import scala.annotation.tailrec import org.knora.webapi.config.AppConfig -import org.knora.webapi.routing.admin.ProjectsRouteZ import org.knora.webapi.slice.resourceinfo.api.ResourceInfoRoute object HttpServerZ { - private val corsMiddleware = - Middleware.cors( - Cors.CorsConfig( - anyOrigin = true, - anyMethod = false, - allowedMethods = Some(Set(Method.GET, Method.PUT, Method.DELETE, Method.POST)) - ) - ) - - private val loggingMiddleware = Middleware.requestLogging() - - private val tracingMiddleware = new Middleware[Any, Nothing, Request, Response, Request, Response] { - override def apply[R1, E1](http: Http[R1, E1, Request, Response])(implicit - trace: Trace - ): Http[R1, E1, Request, Response] = - Http.collectZIO[Request] { case request => - http(request).mapError(_.get) @@ TraceId(UUID.randomUUID()) - } - } - - private def metricsMiddleware() = { - // in order to avoid extensive amounts of labels, we should replace path segment slugs - // see docs/03-endpoints/instrumentation/metrics.md - val slugs = Set( - "iri", - "shortcode", - "shortname" - ) - @tailrec - def replaceSlugs(seg: List[Path.Segment], acc: Path): Path = - seg match { - case head :: _ :: next if slugs.contains(head.text) => replaceSlugs(next, acc / head.text / s":${head.text}") - case head :: next => replaceSlugs(next, acc / head.text) - case Nil => acc - } - Middleware.metrics( - pathLabelMapper = { case request: Request => replaceSlugs(request.path.segments.toList, Path.empty).toString }, - concurrentRequestsName = "zio_http_concurrent_requests_total", - totalRequestsName = "zio_http_requests_total", - requestDurationName = "zio_http_request_duration_seconds" - ) - } - - private val apiRoutes: URIO[ProjectsRouteZ with ResourceInfoRoute, HttpApp[Any, Nothing]] = for { - projectsRoute <- ZIO.service[ProjectsRouteZ].map(_.route) - riRoute <- ZIO.service[ResourceInfoRoute].map(_.route) - } yield projectsRoute ++ riRoute + private val apiRoutes: URIO[ResourceInfoRoute, HttpApp[Any, Nothing]] = for { + riRoute <- ZIO.serviceWith[ResourceInfoRoute](_.route) + } yield riRoute - val layer: ZLayer[ResourceInfoRoute with ProjectsRouteZ with AppConfig, Nothing, Unit] = ZLayer { + val layer: ZLayer[ResourceInfoRoute with AppConfig, Nothing, Unit] = ZLayer { for { - port <- ZIO.service[AppConfig].map(_.knoraApi.externalZioPort) + port <- ZIO.serviceWith[AppConfig](_.knoraApi.externalZioPort) routes <- apiRoutes - metrics = metricsMiddleware() - routesWithMiddleware = routes @@ corsMiddleware @@ metrics @@ loggingMiddleware @@ tracingMiddleware - serverConfig = ZLayer.succeed(ServerConfig.default.port(port)) - _ <- Server.serve(routesWithMiddleware).provide(Server.live, serverConfig).forkDaemon + routesWithMiddleware = routes + _ <- Server.serve(routesWithMiddleware).provide(Server.defaultWithPort(port)).forkDaemon _ <- ZIO.logInfo(">>> Acquire ZIO HTTP Server <<<") } yield () } diff --git a/webapi/src/main/scala/org/knora/webapi/core/InstrumentationServer.scala b/webapi/src/main/scala/org/knora/webapi/core/InstrumentationServer.scala index e65bbaeac3..2f4afb05ea 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/InstrumentationServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/InstrumentationServer.scala @@ -32,15 +32,13 @@ object InstrumentationServer { .service[AppConfig] .flatMap { config => val port = config.instrumentationServerConfig.port - val serverConfig = ServerConfig.default.port(port) val interval = config.instrumentationServerConfig.interval val metricsConfig = MetricsConfig(interval) ZIO.logInfo(s"Starting instrumentation http server on port: $port") *> - ZIO.debug(s"$serverConfig, $metricsConfig") *> instrumentationServer .provideSome[State]( // HTTP Server - ZLayer.succeed(serverConfig) >>> Server.live, + Server.defaultWithPort(port), // HTTP routes IndexApp.layer, HealthRouteZ.layer, 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 01640bcde9..60b40cca05 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -10,7 +10,6 @@ import zio.ZLayer import org.knora.webapi.config.AppConfig import org.knora.webapi.config.AppConfig.AppConfigurations -import org.knora.webapi.http.middleware.AuthenticationMiddleware import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.util._ import org.knora.webapi.messages.util.search.QueryTraverser @@ -28,8 +27,6 @@ import org.knora.webapi.responders.v2.ontology.CardinalityHandlerLive import org.knora.webapi.responders.v2.ontology.OntologyHelpers import org.knora.webapi.responders.v2.ontology.OntologyHelpersLive import org.knora.webapi.routing._ -import org.knora.webapi.routing.admin.AuthenticatorService -import org.knora.webapi.routing.admin.ProjectsRouteZ import org.knora.webapi.slice.admin.api._ import org.knora.webapi.slice.admin.api.service.MaintenanceRestService import org.knora.webapi.slice.admin.api.service.ProjectADMRestService @@ -135,9 +132,7 @@ object LayersLive { ApiRoutes.layer, AppConfig.layer, AppRouter.layer, - AuthenticationMiddleware.layer, AuthenticatorLive.layer, - AuthenticatorService.layer, BaseEndpoints.layer, CacheServiceInMemImpl.layer, CacheServiceRequestMessageHandlerLive.layer, @@ -182,7 +177,6 @@ object LayersLive { ProjectsEndpoints.layer, ProjectsEndpointsHandler.layer, ProjectsResponderADMLive.layer, - ProjectsRouteZ.layer, QueryTraverser.layer, RepositoryUpdater.layer, ResourceInfoRepo.layer, diff --git a/webapi/src/main/scala/org/knora/webapi/http/handler/ExceptionHandlerZ.scala b/webapi/src/main/scala/org/knora/webapi/http/handler/ExceptionHandlerZ.scala index 7b46fd17e7..245a32a939 100644 --- a/webapi/src/main/scala/org/knora/webapi/http/handler/ExceptionHandlerZ.scala +++ b/webapi/src/main/scala/org/knora/webapi/http/handler/ExceptionHandlerZ.scala @@ -7,6 +7,7 @@ package org.knora.webapi.http.handler import org.slf4j.LoggerFactory import spray.json._ +import zio.ZIO import zio.http._ import dsp.errors.RequestRejectedException @@ -22,8 +23,7 @@ object ExceptionHandlerZ { private val logger = LoggerFactory.getLogger(ExceptionHandlerZ.getClass) - def exceptionToJsonHttpResponseZ(ex: Throwable, appConfig: AppConfig): Http[Any, Nothing, Any, Response] = { - + def exceptionToJsonHttpResponseZ(ex: Throwable, appConfig: AppConfig): ZIO[Any, Nothing, Response] = { // Get the HTTP status code that corresponds to the exception. val httpStatus = ApiStatusCodesZ.fromExceptionZ(ex) if (httpStatus.code == 500) { @@ -38,7 +38,7 @@ object ExceptionHandlerZ { val json = JsObject(responseFields).compactPrint // ... and the HTTP status code. - Http.response(Response.json(json).setStatus(httpStatus)) + ZIO.succeed(Response.json(json).withStatus(httpStatus)) } /** diff --git a/webapi/src/main/scala/org/knora/webapi/http/middleware/AuthenticationMiddleware.scala b/webapi/src/main/scala/org/knora/webapi/http/middleware/AuthenticationMiddleware.scala deleted file mode 100644 index e2a32df652..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/http/middleware/AuthenticationMiddleware.scala +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright © 2021 - 2023 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.http.middleware - -import zio._ -import zio.http._ - -import org.knora.webapi.config.AppConfig -import org.knora.webapi.messages.admin.responder.usersmessages.UserADM -import org.knora.webapi.messages.util.KnoraSystemInstances -import org.knora.webapi.routing.admin.AuthenticatorService - -final case class AuthenticationMiddleware(authenticatorService: AuthenticatorService) { - - /** - * An authentication middleware that transforms a HttpApp (Request => Response) - * into a custom Http ((Request, UserADM) => Response). - * This makes the requesting user available in the context of the request in a ZIO HTTP route. - * - * Can be used like this: - * {{{ - * final case class Route(authMiddleware: AuthenticationMiddleware){ - * private val middleware = authMiddleware.authenticationMiddleware - * - * private val authenticatedRoutes: UHttp[(Request, UserADM), Response] = - * Http.collectZIO[(Request, UserADM)] { - * case (Method.GET -> !! / "admin" / "users", user: UserADM) => ??? - * } - * - * val routes = authenticatedRoutes @@ middleware - * } - * }}} - */ - val authenticationMiddleware: UMiddleware[(Request, UserADM), Response, Request, Response] = - Middleware.codecZIO[Request, Response]( - request => - for { - requestingUser <- - authenticatorService - .getUser(request) - .orElseSucceed(KnoraSystemInstances.Users.AnonymousUser) - .tap(u => ZIO.logInfo(s"Authenticated as User: ${u.id} (${u.username})")) - } yield (request, requestingUser), - out => ZIO.succeed(out) - ) -} - -object AuthenticationMiddleware { - val layer: URLayer[AppConfig & AuthenticatorService, AuthenticationMiddleware] = - ZLayer.fromFunction(AuthenticationMiddleware.apply _) -} 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 3c642878c0..0a586b5e58 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 @@ -5,7 +5,7 @@ package org.knora.webapi.http.status -import zio.http.model.Status +import zio.http.Status import dsp.errors._ import org.knora.webapi.store.triplestore.errors.TriplestoreTimeoutException diff --git a/webapi/src/main/scala/org/knora/webapi/instrumentation/health/HealthRouteZ.scala b/webapi/src/main/scala/org/knora/webapi/instrumentation/health/HealthRouteZ.scala index 3c8d243d2a..db0696e159 100644 --- a/webapi/src/main/scala/org/knora/webapi/instrumentation/health/HealthRouteZ.scala +++ b/webapi/src/main/scala/org/knora/webapi/instrumentation/health/HealthRouteZ.scala @@ -7,7 +7,6 @@ package org.knora.webapi.instrumentation.health import zio._ import zio.http._ -import zio.http.model._ import zio.json._ import org.knora.webapi.core.State @@ -19,7 +18,7 @@ import org.knora.webapi.core.domain.AppState final case class HealthRouteZ() { val route: HttpApp[State, Nothing] = - Http.collectZIO[Request] { case Method.GET -> !! / "health" => + Http.collectZIO[Request] { case Method.GET -> Root / "health" => State.getAppState.map(toHealthCheckResult).flatMap(createResponse) } @@ -61,7 +60,7 @@ final case class HealthRouteZ() { ZIO.succeed( Response .json(result.toJson) - .setStatus(statusCode(result.status)) + .withStatus(statusCode(result.status)) ) /** diff --git a/webapi/src/main/scala/org/knora/webapi/instrumentation/index/IndexApp.scala b/webapi/src/main/scala/org/knora/webapi/instrumentation/index/IndexApp.scala index 2da616fc10..6069c716a7 100644 --- a/webapi/src/main/scala/org/knora/webapi/instrumentation/index/IndexApp.scala +++ b/webapi/src/main/scala/org/knora/webapi/instrumentation/index/IndexApp.scala @@ -8,7 +8,6 @@ package org.knora.webapi.instrumentation.index import zio._ import zio.http._ import zio.http.html.Html -import zio.http.model._ /** * Provides the '/' endpoint serving a small index page. @@ -16,7 +15,7 @@ import zio.http.model._ final case class IndexApp() { val route: HttpApp[Any, Nothing] = - Http.collect[Request] { case Method.GET -> !! => Response.html(Html.fromString(indexPage)) } + Http.collect[Request] { case Method.GET -> Root => Response.html(Html.fromString(indexPage)) } private val indexPage = """ diff --git a/webapi/src/main/scala/org/knora/webapi/instrumentation/prometheus/PrometheusApp.scala b/webapi/src/main/scala/org/knora/webapi/instrumentation/prometheus/PrometheusApp.scala index 0427b1981e..ab75539756 100644 --- a/webapi/src/main/scala/org/knora/webapi/instrumentation/prometheus/PrometheusApp.scala +++ b/webapi/src/main/scala/org/knora/webapi/instrumentation/prometheus/PrometheusApp.scala @@ -7,7 +7,6 @@ package org.knora.webapi.instrumentation.prometheus import zio._ import zio.http._ -import zio.http.model._ import zio.metrics.connectors.prometheus.PrometheusPublisher /** @@ -17,11 +16,10 @@ final case class PrometheusApp() { val route: HttpApp[PrometheusPublisher, Nothing] = Http - .collectZIO[Request] { case Method.GET -> !! / "metrics" => + .collectZIO[Request] { case Method.GET -> Root / "metrics" => ZIO.serviceWithZIO[PrometheusPublisher](_.get.map(Response.text)) } } object PrometheusApp { - val layer = - ZLayer.succeed(PrometheusApp()) + val layer = ZLayer.succeed(PrometheusApp()) } 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 deleted file mode 100644 index 460246da39..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/AuthenticatorService.scala +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright © 2021 - 2023 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 zio._ -import zio.http._ - -import org.knora.webapi.messages.admin.responder.usersmessages._ -import org.knora.webapi.messages.util.KnoraSystemInstances - -trait AuthenticatorService { - def getUser(request: Request): Task[UserADM] -} - -object AuthenticatorService { - val layer = ZLayer.fromFunction(AuthenticatorServiceLive.apply _) - - def mock(user: Option[UserADM] = None) = ZLayer( - ZIO.succeed( - new AuthenticatorService() { - override def getUser(request: Request): Task[UserADM] = - ZIO.attempt(user.getOrElse(KnoraSystemInstances.Users.AnonymousUser)) - } - ) - ) -} 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 deleted file mode 100644 index 8b1b3c3774..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/AuthenticatorServiceLive.scala +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright © 2021 - 2023 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 zio._ -import zio.http._ - -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.routing.Authenticator -import org.knora.webapi.routing.admin.AuthenticatorServiceLive.extractCredentialsFromRequest - -final case class AuthenticatorServiceLive( - private val authenticator: Authenticator, - private implicit val stringFormatter: StringFormatter -) extends AuthenticatorService { - - private val authCookieName = authenticator.calculateCookieName() - - override def getUser(request: Request): Task[UserADM] = - extractCredentialsFromRequest(request, authCookieName).flatMap(authenticator.getUserADMThroughCredentialsV2) -} - -object AuthenticatorServiceLive { - - // visible for testing - def extractCredentialsFromRequest(request: Request, cookieName: String)(implicit - sf: StringFormatter - ): Task[Option[KnoraCredentialsV2]] = - ZIO.attempt( - 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/ProjectsRouteZ.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala deleted file mode 100644 index ebfb90565f..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Copyright © 2021 - 2023 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 zio._ -import zio.http._ -import zio.http.model._ -import zio.json._ -import zio.stream.ZStream - -import java.nio.file.Files - -import dsp.errors.BadRequestException -import dsp.valueobjects.Iri._ -import org.knora.webapi.config.AppConfig -import org.knora.webapi.http.handler.ExceptionHandlerZ -import org.knora.webapi.http.middleware.AuthenticationMiddleware -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM._ -import org.knora.webapi.messages.admin.responder.usersmessages.UserADM -import org.knora.webapi.routing.RouteUtilZ -import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectCreateRequest -import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectSetRestrictedViewSizeRequest -import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectUpdateRequest -import org.knora.webapi.slice.admin.api.service.ProjectADMRestService - -final case class ProjectsRouteZ( - appConfig: AppConfig, - projectsService: ProjectADMRestService, - authenticationMiddleware: AuthenticationMiddleware -) { - - lazy val route: HttpApp[Any, Nothing] = projectRoutes @@ authenticationMiddleware.authenticationMiddleware - - private val projectRoutes: Http[Any, Nothing, (Request, UserADM), Response] = - Http - .collectZIO[(Request, UserADM)] { - // project crud - case (Method.GET -> !! / "admin" / "projects", _) => getProjects() - case (Method.GET -> !! / "admin" / "projects" / "iri" / iriUrlEncoded, _) => getProjectByIri(iriUrlEncoded) - case (Method.GET -> !! / "admin" / "projects" / "shortname" / shortname, _) => getProjectByShortname(shortname) - case (Method.GET -> !! / "admin" / "projects" / "shortcode" / shortcode, _) => getProjectByShortcode(shortcode) - case (request @ Method.POST -> !! / "admin" / "projects", requestingUser) => - createProject(request, requestingUser) - case (Method.DELETE -> !! / "admin" / "projects" / "iri" / iriUrlEncoded, requestingUser) => - deleteProject(iriUrlEncoded, requestingUser) - case (request @ Method.PUT -> !! / "admin" / "projects" / "iri" / iriUrlEncoded, requestingUser) => - updateProject(iriUrlEncoded, request, requestingUser) - case (Method.GET -> !! / "admin" / "projects" / "iri" / iriUrlEncoded / "AllData", requestingUser) => - getAllProjectData(iriUrlEncoded, requestingUser) - - // export/import endpoints - case (Method.GET -> !! / "admin" / "projects" / "exports", requestingUser) => - getProjectExports(requestingUser) - case (Method.POST -> !! / "admin" / "projects" / "shortcode" / shortcode / "import", requestingUser) => - postImportProject(shortcode, requestingUser) - case (Method.POST -> !! / "admin" / "projects" / "shortcode" / shortcode / "export", requestingUser) => - postExportProject(shortcode, requestingUser) - - // project members - case (Method.GET -> !! / "admin" / "projects" / "iri" / iriUrlEncoded / "members", requestingUser) => - getProjectMembersByIri(iriUrlEncoded, requestingUser) - case (Method.GET -> !! / "admin" / "projects" / "shortname" / shortname / "members", requestingUser) => - getProjectMembersByShortname(shortname, requestingUser) - case (Method.GET -> !! / "admin" / "projects" / "shortcode" / shortcode / "members", requestingUser) => - getProjectMembersByShortcode(shortcode, requestingUser) - case (Method.GET -> !! / "admin" / "projects" / "iri" / iriUrlEncoded / "admin-members", requestingUser) => - getProjectAdminsByIri(iriUrlEncoded, requestingUser) - case (Method.GET -> !! / "admin" / "projects" / "shortname" / shortname / "admin-members", requestingUser) => - getProjectAdminsByShortname(shortname, requestingUser) - case (Method.GET -> !! / "admin" / "projects" / "shortcode" / shortcode / "admin-members", requestingUser) => - getProjectAdminsByShortcode(shortcode, requestingUser) - - // keywords - case (Method.GET -> !! / "admin" / "projects" / "Keywords", _) => getKeywords() - case (Method.GET -> !! / "admin" / "projects" / "iri" / iriUrlEncoded / "Keywords", _) => - getKeywordsByProjectIri(iriUrlEncoded) - - // view settings - case (Method.GET -> !! / "admin" / "projects" / "iri" / iriUrlEncoded / "RestrictedViewSettings", _) => - getRestrictedViewSettingsByProjectIri(iriUrlEncoded) - case (Method.GET -> !! / "admin" / "projects" / "shortname" / shortname / "RestrictedViewSettings", _) => - getRestrictedViewSettingsByShortname(shortname) - case (Method.GET -> !! / "admin" / "projects" / "shortcode" / shortcode / "RestrictedViewSettings", _) => - getRestrictedViewSettingsByShortcode(shortcode) - case ( - request @ Method.POST -> !! / "admin" / "projects" / "iri" / iri / "RestrictedViewSettings", - requestingUser - ) => - setProjectRestrictedViewSizeByIri(iri, request.body, requestingUser) - case ( - request @ Method.POST -> !! / "admin" / "projects" / "shortcode" / shortcode / "RestrictedViewSettings", - requstingUser - ) => - setProjectRestrictedViewSizeByShortcode(shortcode, request.body, requstingUser) - } - .catchAll(ExceptionHandlerZ.exceptionToJsonHttpResponseZ(_, appConfig)) - - private def getProjects(): Task[Response] = - for { - r <- projectsService.listAllProjects() - } yield Response.json(r.toJsValue.toString) - - private def getProjectByIri(iriUrlEncoded: String): Task[Response] = - for { - iriDecoded <- RouteUtilZ.urlDecode(iriUrlEncoded, s"Failed to URL decode IRI parameter $iriUrlEncoded.") - iriIdentifier <- IriIdentifier.fromString(iriDecoded).toZIO.mapError(e => BadRequestException(e.msg)) - r <- projectsService.findProject(iriIdentifier) - } yield Response.json(r.toJsValue.toString) - - private def getProjectByShortname(shortname: String): Task[Response] = - for { - shortnameIdentifier <- ShortnameIdentifier.fromString(shortname).toZIO.mapError(e => BadRequestException(e.msg)) - r <- projectsService.findProject(shortnameIdentifier) - } yield Response.json(r.toJsValue.toString) - - private def getProjectByShortcode(shortcode: String): Task[Response] = - for { - shortcodeIdentifier <- ShortcodeIdentifier.fromString(shortcode).toZIO.mapError(e => BadRequestException(e.msg)) - r <- projectsService.findProject(shortcodeIdentifier) - } yield Response.json(r.toJsValue.toString()) - - private def createProject(request: Request, requestingUser: UserADM): Task[Response] = - for { - body <- request.body.asString - payload <- ZIO.fromEither(body.fromJson[ProjectCreateRequest]).mapError(e => BadRequestException(e)) - r <- projectsService.createProject(payload, requestingUser) - } yield Response.json(r.toJsValue.toString) - - private def deleteProject(iriUrlEncoded: String, requestingUser: UserADM): Task[Response] = - for { - iriDecoded <- RouteUtilZ.urlDecode(iriUrlEncoded, s"Failed to URL decode IRI parameter $iriUrlEncoded.") - id <- IriIdentifier.fromString(iriDecoded).toZIO.mapError(e => BadRequestException(e.msg)) - response <- projectsService.deleteProject(id, requestingUser) - } yield Response.json(response.toJsValue.toString()) - - private def updateProject(iriUrlEncoded: String, request: Request, requestingUser: UserADM): Task[Response] = - for { - iriDecoded <- RouteUtilZ.urlDecode(iriUrlEncoded, s"Failed to URL decode IRI parameter $iriUrlEncoded.") - projectIri <- ProjectIri.make(iriDecoded).toZIO.mapBoth(e => BadRequestException(e.msg), IriIdentifier.from) - body <- request.body.asString - payload <- ZIO.fromEither(body.fromJson[ProjectUpdateRequest]).mapError(e => BadRequestException(e)) - r <- projectsService.updateProject(projectIri, payload, requestingUser) - } yield Response.json(r.toJsValue.toString) - - private def getAllProjectData(iriUrlEncoded: String, requestingUser: UserADM): Task[Response] = - for { - iriDecoded <- RouteUtilZ.urlDecode(iriUrlEncoded, s"Failed to URL decode IRI parameter $iriUrlEncoded.") - iriIdentifier <- IriIdentifier.fromString(iriDecoded).toZIO.mapError(e => BadRequestException(e.msg)) - projectDataGetResponse <- projectsService.getAllProjectData(iriIdentifier, requestingUser) - filePath = projectDataGetResponse.projectDataFile - fileStream = ZStream - .fromPath(filePath) - .ensuring( - ZIO - .attempt(Files.deleteIfExists(filePath)) - .orDie - .logError(s"File couldn't be deleted: ${filePath.toString()}") - ) - r = Response( - headers = Headers.contentType("application/trig"), - body = Body.fromStream(fileStream) - ) - } yield r - - private def postExportProject(shortcode: String, requestingUser: UserADM): Task[Response] = - projectsService - .exportProject(shortcode, requestingUser) - .as(Response.text("work in progress").setStatus(Status.Accepted)) - - private def postImportProject(shortcode: String, requestingUser: UserADM): Task[Response] = - projectsService.importProject(shortcode, requestingUser).map(_.toJson).map(Response.json(_)) - - private def getProjectExports(requestingUser: UserADM): Task[Response] = - projectsService.listExports(requestingUser).map(_.toJson).map(Response.json(_)) - - private def getProjectMembersByIri(iriUrlEncoded: String, requestingUser: UserADM): Task[Response] = - for { - iriDecoded <- RouteUtilZ.urlDecode(iriUrlEncoded, s"Failed to URL decode IRI parameter $iriUrlEncoded.") - iriIdentifier <- IriIdentifier.fromString(iriDecoded).toZIO.mapError(e => BadRequestException(e.msg)) - r <- projectsService.getProjectMembers(requestingUser, iriIdentifier) - } yield Response.json(r.toJsValue.toString) - - private def getProjectMembersByShortname(shortname: String, requestingUser: UserADM): Task[Response] = - for { - shortnameIdentifier <- ShortnameIdentifier.fromString(shortname).toZIO.mapError(e => BadRequestException(e.msg)) - r <- projectsService.getProjectMembers(requestingUser, shortnameIdentifier) - } yield Response.json(r.toJsValue.toString) - - private def getProjectMembersByShortcode(shortcode: String, requestingUser: UserADM): Task[Response] = - for { - shortcodeIdentifier <- ShortcodeIdentifier.fromString(shortcode).toZIO.mapError(e => BadRequestException(e.msg)) - r <- projectsService.getProjectMembers(requestingUser, shortcodeIdentifier) - } yield Response.json(r.toJsValue.toString()) - - private def getProjectAdminsByIri(iriUrlEncoded: String, requestingUser: UserADM): Task[Response] = - for { - iriDecoded <- RouteUtilZ.urlDecode(iriUrlEncoded, s"Failed to URL decode IRI parameter $iriUrlEncoded.") - iriIdentifier <- IriIdentifier.fromString(iriDecoded).toZIO.mapError(e => BadRequestException(e.msg)) - r <- projectsService.getProjectAdminMembers(requestingUser, iriIdentifier) - } yield Response.json(r.toJsValue.toString) - - private def getProjectAdminsByShortname(shortname: String, requestingUser: UserADM): Task[Response] = - for { - shortnameIdentifier <- ShortnameIdentifier.fromString(shortname).toZIO.mapError(e => BadRequestException(e.msg)) - r <- projectsService.getProjectAdminMembers(requestingUser, shortnameIdentifier) - } yield Response.json(r.toJsValue.toString) - - private def getProjectAdminsByShortcode(shortcode: String, requestingUser: UserADM): Task[Response] = - for { - shortcodeIdentifier <- ShortcodeIdentifier.fromString(shortcode).toZIO.mapError(e => BadRequestException(e.msg)) - r <- projectsService.getProjectAdminMembers(requestingUser, shortcodeIdentifier) - } yield Response.json(r.toJsValue.toString()) - - private def getKeywords(): Task[Response] = - for { - r <- projectsService.listAllKeywords() - } yield Response.json(r.toJsValue.toString) - - private def getKeywordsByProjectIri(iriUrlEncoded: String): Task[Response] = - for { - iriDecoded <- RouteUtilZ.urlDecode(iriUrlEncoded, s"Failed to URL decode IRI parameter $iriUrlEncoded.") - projectIri <- ProjectIri.make(iriDecoded).toZIO.mapError(e => BadRequestException(e.msg)) - r <- projectsService.getKeywordsByProjectIri(projectIri) - } yield Response.json(r.toJsValue.toString) - - private def getRestrictedViewSettingsByProjectIri(iriUrlEncoded: String): Task[Response] = - for { - iriDecoded <- RouteUtilZ.urlDecode(iriUrlEncoded, s"Failed to URL decode IRI parameter $iriUrlEncoded.") - iriIdentifier <- IriIdentifier.fromString(iriDecoded).toZIO.mapError(e => BadRequestException(e.msg)) - r <- projectsService.getProjectRestrictedViewSettings(iriIdentifier) - } yield Response.json(r.toJsValue.toString) - - private def getRestrictedViewSettingsByShortname(shortname: String): Task[Response] = - for { - shortnameIdentifier <- ShortnameIdentifier.fromString(shortname).toZIO.mapError(e => BadRequestException(e.msg)) - r <- projectsService.getProjectRestrictedViewSettings(shortnameIdentifier) - } yield Response.json(r.toJsValue.toString) - - private def getRestrictedViewSettingsByShortcode(shortcode: String): Task[Response] = - for { - shortcodeIdentifier <- ShortcodeIdentifier.fromString(shortcode).toZIO.mapError(e => BadRequestException(e.msg)) - r <- projectsService.getProjectRestrictedViewSettings(shortcodeIdentifier) - } yield Response.json(r.toJsValue.toString) - - private def setProjectRestrictedViewSizeByIri(iri: IRI, body: Body, user: UserADM): Task[Response] = - for { - iriDecoded <- RouteUtilZ.urlDecode(iri, s"Failed to URL decode IRI parameter $iri.") - id <- IriIdentifier.fromString(iriDecoded).toZIO.mapError(e => BadRequestException(e.msg)) - result <- handleRestrictedViewSizeRequest(id, body, user) - } yield result - - private def setProjectRestrictedViewSizeByShortcode(shortcode: String, body: Body, user: UserADM): Task[Response] = - for { - id <- ShortcodeIdentifier.fromString(shortcode).toZIO.mapError(e => BadRequestException(e.msg)) - result <- handleRestrictedViewSizeRequest(id, body, user) - } yield result - - private def handleRestrictedViewSizeRequest(id: ProjectIdentifierADM, body: Body, user: UserADM) = - for { - body <- body.asString - payload <- ZIO.fromEither(body.fromJson[ProjectSetRestrictedViewSizeRequest]).mapError(BadRequestException(_)) - response <- projectsService.updateProjectRestrictedViewSettings(id, user, payload) - } yield Response.json(response.toJson) -} - -object ProjectsRouteZ { - val layer: URLayer[AppConfig with ProjectADMRestService with AuthenticationMiddleware, ProjectsRouteZ] = - ZLayer.fromFunction(ProjectsRouteZ.apply _) -} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/DspIngestClient.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/DspIngestClient.scala index a5320fab10..e114151c13 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/DspIngestClient.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/DspIngestClient.scala @@ -16,9 +16,11 @@ import zio.ZIO import zio.ZLayer import zio.http.Body import zio.http.Client +import zio.http.Header +import zio.http.Headers +import zio.http.MediaType import zio.http.Request import zio.http.URL -import zio.http.model.Headers import zio.macros.accessible import zio.nio.file.Files import zio.nio.file.Path @@ -61,12 +63,16 @@ final case class DspIngestClientLive( def importProject(shortcode: Shortcode, fileToImport: Path): Task[Path] = ZIO.scoped { for { - importUrl <- ZIO.fromEither(URL.fromString(s"${projectsPath(shortcode)}/import")) + importUrl <- ZIO.fromEither(URL.decode(s"${projectsPath(shortcode)}/import")) token <- jwtService.createJwtForDspIngest() request = Request .post(Body.fromFile(fileToImport.toFile), importUrl) - .updateHeaders(_.addHeaders(Headers.bearerAuthorizationHeader(token.jwtString))) - .updateHeaders(_.addHeaders(Headers.contentType("application/zip"))) + .addHeaders( + Headers( + Header.Authorization.Bearer(token.jwtString), + Header.ContentType(MediaType.application.zip) + ) + ) response <- Client.request(request).provideSomeLayer[Scope](Client.default) bodyAsString <- response.body.asString _ <- ZIO.logInfo(s"Response code: ${response.status} body $bodyAsString") diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectImportService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectImportService.scala index 1ff5a6bb7f..df26d76703 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectImportService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectImportService.scala @@ -46,7 +46,7 @@ final case class ProjectImportServiceLive( private val fusekiBaseUrl: URL = { val str = config.host + ":" + config.fuseki.port val protocol = if (config.useHttps) "https://" else "http://" - URL.fromString(protocol + str).getOrElse(throw new IllegalStateException(s"Invalid fuseki url: $str")) + URL.decode(protocol + str).getOrElse(throw new IllegalStateException(s"Invalid fuseki url: $str")) } private val httpClient = { val basicAuth = new Authenticator { @@ -60,7 +60,7 @@ final case class ProjectImportServiceLive( def acquire(fusekiUrl: URL, httpClient: HttpClient) = ZIO.attempt(RDFConnectionFuseki.service(fusekiUrl.encode).httpClient(httpClient).build()) def release(connection: RDFConnection) = ZIO.attempt(connection.close()).logError.ignore - ZIO.acquireRelease(acquire(fusekiBaseUrl.setPath(s"/${config.fuseki.repositoryName}"), httpClient))(release) + ZIO.acquireRelease(acquire(fusekiBaseUrl.withPath(s"/${config.fuseki.repositoryName}"), httpClient))(release) } override def importTrigFile(file: Path): Task[Unit] = ZIO.scoped { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/ResourceInfoRoute.scala b/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/ResourceInfoRoute.scala index 17cb1ebace..d6549e9c23 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/ResourceInfoRoute.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/ResourceInfoRoute.scala @@ -6,9 +6,8 @@ package org.knora.webapi.slice.resourceinfo.api import zio.ZLayer +import zio.http.HttpError.BadRequest import zio.http._ -import zio.http.model.HttpError.BadRequest -import zio.http.model._ import zio.json.EncoderOps import zio.prelude.Validation @@ -22,7 +21,7 @@ import org.knora.webapi.slice.resourceinfo.api.RestResourceInfoServiceLive.lastM final case class ResourceInfoRoute(restService: RestResourceInfoService) { val route: HttpApp[Any, Nothing] = - Http.collectZIO[Request] { case req @ Method.GET -> !! / "v2" / "resources" / "info" => + Http.collectZIO[Request] { case req @ Method.GET -> Root / "v2" / "resources" / "info" => (for { p <- getParameters(req) result <- restService.findByProjectAndResourceClass(p._1, p._2, (p._3, p._4)) @@ -74,9 +73,9 @@ final case class ResourceInfoRoute(restService: RestResourceInfoService) { } private def getProjectIri(headers: Headers) = { - val projectIri: Validation[BadRequest, IRI] = headers.header(RouteUtilV2.PROJECT_HEADER) match { - case None => Validation.fail(BadRequest(s"Header ${RouteUtilV2.PROJECT_HEADER} may not be empty")) - case Some(header) => Validation.succeed(header.value.toString) + val projectIri: Validation[BadRequest, IRI] = headers.get(RouteUtilV2.PROJECT_HEADER) match { + case None => Validation.fail(BadRequest(s"Header ${RouteUtilV2.PROJECT_HEADER} may not be empty")) + case Some(value) => Validation.succeed(value) } projectIri } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/RestResourceInfoService.scala b/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/RestResourceInfoService.scala index 9a22eb3033..1a283112bd 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/RestResourceInfoService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/RestResourceInfoService.scala @@ -6,7 +6,7 @@ package org.knora.webapi.slice.resourceinfo.api import zio._ -import zio.http.model.HttpError +import zio.http.HttpError import org.knora.webapi.IRI import org.knora.webapi.slice.resourceinfo.api.RestResourceInfoServiceLive.Order diff --git a/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/RestResourceInfoServiceLive.scala b/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/RestResourceInfoServiceLive.scala index 793932abac..37c512d76c 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/RestResourceInfoServiceLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/resourceinfo/api/RestResourceInfoServiceLive.scala @@ -6,7 +6,7 @@ package org.knora.webapi.slice.resourceinfo.api import zio.IO -import zio.http.model.HttpError +import zio.http.HttpError import java.time.Instant diff --git a/webapi/src/test/scala/org/knora/webapi/http/middleware/AuthenticationMiddlewareSpec.scala b/webapi/src/test/scala/org/knora/webapi/http/middleware/AuthenticationMiddlewareSpec.scala deleted file mode 100644 index 30f6e96650..0000000000 --- a/webapi/src/test/scala/org/knora/webapi/http/middleware/AuthenticationMiddlewareSpec.scala +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright © 2021 - 2023 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.http.middleware - -import zio._ -import zio.http._ -import zio.test._ - -import org.knora.webapi.messages.admin.responder.groupsmessages.GroupADM -import org.knora.webapi.messages.admin.responder.permissionsmessages.PermissionsDataADM -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM -import org.knora.webapi.messages.admin.responder.usersmessages.UserADM -import org.knora.webapi.messages.util.KnoraSystemInstances -import org.knora.webapi.routing.admin.AuthenticatorService - -object AuthenticationMiddlewareSpec extends ZIOSpecDefault { - private val passUserThroughApp: Http[Any, Nothing, (Request, UserADM), Response] = - Http.collect[(Request, UserADM)] { case (_, user) => Response.text(user.id) } - - private val anonymousUser = KnoraSystemInstances.Users.AnonymousUser - private val someUser = UserADM( - id = "http://rdfh.ch/users/someuser", - username = "someuser", - email = "some.user@example.com", - givenName = "Some", - familyName = "User", - status = true, - lang = "en", - password = Some("$2a$12$7XEBehimXN1rbhmVgQsyve08.vtDmKK7VMin4AdgCEtE4DWgfQbTK"), - token = None, - groups = Seq.empty[GroupADM], - projects = Seq.empty[ProjectADM], - permissions = PermissionsDataADM() - ) - - private val authServiceFailing = new AuthenticatorService { - override def getUser(request: Request): Task[UserADM] = ZIO.fail(new RuntimeException("")) - } - private val authServiceAnonymous = new AuthenticatorService { - override def getUser(request: Request): Task[UserADM] = ZIO.succeed(anonymousUser) - } - private val authServiceSome = new AuthenticatorService { - override def getUser(request: Request): Task[UserADM] = ZIO.succeed(someUser) - } - - val authenticationSuccessSuite = suite("when authentication succeeds")( - test("should return anonymous user if no authentication was provided") { - val middleware = AuthenticationMiddleware(authServiceAnonymous).authenticationMiddleware - val app = passUserThroughApp @@ middleware - for { - response <- app.apply(Request.get(URL.empty)) - resId <- response.body.asString - } yield assertTrue(resId == anonymousUser.id) - }, - test("should return the requesting user if valid authentication was provided") { - val middleware = AuthenticationMiddleware(authServiceSome).authenticationMiddleware - val app = passUserThroughApp @@ middleware - for { - response <- app.apply(Request.get(URL.empty)) - resId <- response.body.asString - } yield assertTrue(resId == someUser.id) - } - ) - - val authenticationFailureSuite = suite("when authentication fails")( - test("should still return anonymous user") { - val middleware = AuthenticationMiddleware(authServiceFailing).authenticationMiddleware - val app = passUserThroughApp @@ middleware - for { - response <- app.apply(Request.get(URL.empty)) - resId <- response.body.asString - } yield assertTrue(resId == anonymousUser.id) - } - ) - - override def spec = suite("AuthenticationMiddleware")( - authenticationSuccessSuite, - authenticationFailureSuite - ) - -} 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 deleted file mode 100644 index 674228364d..0000000000 --- a/webapi/src/test/scala/org/knora/webapi/routing/admin/AuthenticatorServiceLiveSpec.scala +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright © 2021 - 2023 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 zio.http._ -import zio.http.model._ -import zio.test.Assertion._ -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.getInitializedTestInstance - - val headerInvalidSuite = suite("given invalid header authentication")( - test("should fail to extract user email (basic auth)") { - val userMail = "user.example.com" - val req = Request.get(URL.empty).setHeaders(Headers.basicAuthorizationHeader(userMail, "pass")) - for { - actual <- AuthenticatorServiceLive.extractCredentialsFromRequest(req, cookieName).exit - } yield assert(actual)(fails(hasMessage(equalTo(s"Invalid email $userMail")))) - } - ) - - val headerValidSuite = suite("given valid header authentication")( - test("should extract user email (basic auth)") { - val userMail = "user@example.com" - val req = Request.get(URL.empty).setHeaders(Headers.basicAuthorizationHeader(userMail, "pass")) - for { - actual <- AuthenticatorServiceLive.extractCredentialsFromRequest(req, cookieName) - expected = Some(KnoraPasswordCredentialsV2(UserIdentifierADM(maybeEmail = Some(userMail)), "pass")) - } yield assertTrue(actual == expected) - }, - test("should extract jwt token (bearer token)") { - val jwtToken = "someToken" - val req = Request.get(URL.empty).setHeaders(Headers.bearerAuthorizationHeader(jwtToken)) - for { - actual <- AuthenticatorServiceLive.extractCredentialsFromRequest(req, cookieName) - expected = Some(KnoraJWTTokenCredentialsV2(jwtToken)) - } yield assertTrue(actual == expected) - }, - test("should extract session cookie") { - val sessionCookieValue = "session" - val req = - Request - .get(URL.empty) - .setHeaders(Headers.cookie(Cookie(cookieName, sessionCookieValue, Cookie.Type.request))) - for { - actual <- AuthenticatorServiceLive.extractCredentialsFromRequest(req, cookieName) - expected = Some(KnoraSessionCredentialsV2(sessionCookieValue)) - } yield assertTrue(actual == expected) - } - ) - - val queryParamInvalidSuite = suite("given invalid query parameters authentication")( - test("should fail if different credentials are provided") { - val params = QueryParams( - ("username", "someUsername"), - ("email", "user@example.com"), - ("password", "somePassword") - ) - val req = Request.get(URL.empty.setQueryParams(params)) - for { - actual <- AuthenticatorServiceLive.extractCredentialsFromRequest(req, cookieName).exit - } yield assert(actual)(fails(hasMessage(equalTo("Only one option allowed for user identifier.")))) - }, - test("should fail if if an invalid email is provided") { - val params = QueryParams( - ("email", "user.example.com"), - ("password", "somePassword") - ) - val req = Request.get(URL.empty.setQueryParams(params)) - for { - actual <- AuthenticatorServiceLive.extractCredentialsFromRequest(req, cookieName).exit - } yield assert(actual)(fails(hasMessage(equalTo("Invalid email user.example.com")))) - }, - test("should fail if if an invalid username is provided") { - val params = QueryParams( - ("username", "some\rUser"), - ("password", "somePassword") - ) - val req = Request.get(URL.empty.setQueryParams(params)) - for { - actual <- AuthenticatorServiceLive.extractCredentialsFromRequest(req, cookieName).exit - } yield assert(actual)(fails(hasMessage(equalTo("Invalid username Some(some\rUser)")))) - }, - test("should fail if if an invalid IRI is provided") { - val params = QueryParams( - ("iri", "notAnIri"), - ("password", "somePassword") - ) - val req = Request.get(URL.empty.setQueryParams(params)) - for { - actual <- AuthenticatorServiceLive.extractCredentialsFromRequest(req, cookieName).exit - } yield assert(actual)(fails(hasMessage(equalTo("Invalid user IRI notAnIri")))) - } - ) - - val queryParamValidSuite = suite("given valid query parameters authentication")( - test("should extract jwt token") { - val jwtToken = "someToken" - val req = Request.get(URL.empty.setQueryParams(QueryParams(("token", jwtToken)))) - for { - actual <- AuthenticatorServiceLive.extractCredentialsFromRequest(req, cookieName) - expected = Some(KnoraJWTTokenCredentialsV2(jwtToken)) - } yield assertTrue(actual == expected) - }, - test("should extract username and password") { - val username = "someUsername" - val password = "somePassword" - val params = QueryParams( - ("username", username), - ("password", password) - ) - val req = Request.get(URL.empty.setQueryParams(params)) - for { - actual <- AuthenticatorServiceLive.extractCredentialsFromRequest(req, cookieName) - expected = Some(KnoraPasswordCredentialsV2(UserIdentifierADM(maybeUsername = Some(username)), password)) - } yield assertTrue(actual == expected) - }, - test("should extract email and password") { - val email = "user@example.com" - val password = "somePassword" - val params = QueryParams( - ("email", email), - ("password", password) - ) - val req = Request.get(URL.empty.setQueryParams(params)) - for { - actual <- AuthenticatorServiceLive.extractCredentialsFromRequest(req, cookieName) - expected = Some(KnoraPasswordCredentialsV2(UserIdentifierADM(maybeEmail = Some(email)), password)) - } yield assertTrue(actual == expected) - }, - test("should extract iri and password") { - val userIri = "http://rdfh.ch/users/someUser" - val password = "somePassword" - val params = QueryParams( - ("iri", userIri), - ("password", password) - ) - val req = Request.get(URL.empty.setQueryParams(params)) - for { - actual <- AuthenticatorServiceLive.extractCredentialsFromRequest(req, cookieName) - expected = Some(KnoraPasswordCredentialsV2(UserIdentifierADM(maybeIri = Some(userIri)), password)) - } yield assertTrue(actual == expected) - } - ) - - val spec = suite("AuthenticatorServiceLiveSpec")( - suite("given header authentication")(headerValidSuite, headerInvalidSuite), - suite("given query parameters authentication")(queryParamValidSuite, queryParamInvalidSuite), - suite("when nothing is given")( - test("should return None") { - for { - actual <- AuthenticatorServiceLive.extractCredentialsFromRequest(Request.get(URL.empty), cookieName) - } yield assertTrue(actual.isEmpty) - } - ) - ) -} diff --git a/webapi/src/test/scala/org/knora/webapi/routing/admin/ProjectsRouteZSpec.scala b/webapi/src/test/scala/org/knora/webapi/routing/admin/ProjectsRouteZSpec.scala deleted file mode 100644 index 1aa88dc2af..0000000000 --- a/webapi/src/test/scala/org/knora/webapi/routing/admin/ProjectsRouteZSpec.scala +++ /dev/null @@ -1,758 +0,0 @@ -/* - * Copyright © 2021 - 2023 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 zio._ -import zio.http._ -import zio.http.model.Status -import zio.mock.Expectation -import zio.test._ - -import java.net.URLEncoder -import java.nio.file - -import dsp.valueobjects.V2 -import org.knora.webapi.TestDataFactory -import org.knora.webapi.config.AppConfig -import org.knora.webapi.http.middleware.AuthenticationMiddleware -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.IriIdentifier -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectKeywordsGetResponseADM -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectsKeywordsGetResponseADM -import org.knora.webapi.messages.admin.responder.projectsmessages._ -import org.knora.webapi.messages.admin.responder.usersmessages.UserADM -import org.knora.webapi.messages.util.KnoraSystemInstances -import org.knora.webapi.responders.admin.ProjectADMRestServiceMock -import org.knora.webapi.slice.admin.api.model.ProjectDataGetResponseADM -import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectCreateRequest -import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectUpdateRequest -import org.knora.webapi.slice.admin.api.service.ProjectADMRestService - -object ProjectsRouteZSpec extends ZIOSpecDefault { - - /** - * Paths - */ - private val basePathProjects: Path = !! / "admin" / "projects" - private val basePathProjectsIri: Path = !! / "admin" / "projects" / "iri" - private val basePathProjectsShortname: Path = !! / "admin" / "projects" / "shortname" - private val basePathProjectsShortcode: Path = !! / "admin" / "projects" / "shortcode" - - /** - * Creates a [[ProjectADM]] with empty content or optionally with a given ID. - */ - private def getProjectADM(id: String = "") = - ProjectADM( - id = id, - shortname = "", - shortcode = "", - longname = None, - description = Seq(V2.StringLiteralV2("", None)), - keywords = Seq.empty, - logo = None, - ontologies = Seq.empty, - status = true, - selfjoin = false - ) - - /** - * Returns a ZIO effect that requires a [[ProjectADMRestService]] (so that a mock can be provided) that applies the - * provided [[Request]] to the `routes` of a [[ProjectsRouteZ]], returning a [[Response]]. - */ - private def applyRoutes(request: Request): ZIO[ProjectADMRestService, Option[Nothing], Response] = ZIO - .serviceWithZIO[ProjectsRouteZ](_.route.apply(request)) - .provideSome[ProjectADMRestService]( - AppConfig.layer, - AuthenticationMiddleware.layer, - AuthenticatorService.mock(Some(KnoraSystemInstances.Users.SystemUser)), - ProjectsRouteZ.layer - ) - - /** - * URL encodes a string, assuming utf-8 - */ - private def encode(iri: String) = URLEncoder.encode(iri, "utf-8") - - def spec: Spec[TestEnvironment with Scope, Any] = suite("ProjectsRouteZ")( - getProjectsSpec, - getSingleProjectSpec, - createProjectSpec, - deleteProjectSpec, - updateProjectSpec, - getAllDataSpec, - getProjectMembersSpec, - getProjectAdminsSpec, - getKeywordsSpec, - getKeywordsByProjectSpec, - getProjectRestrictedViewSettings - ) - - val getProjectsSpec: Spec[Any, Serializable] = test("get all projects") { - val request = Request.get(url = URL(basePathProjects)) - val expectedResult = Expectation.value[ProjectsGetResponseADM](ProjectsGetResponseADM(Seq(getProjectADM()))) - val mockService = ProjectADMRestServiceMock.GetProjects(expectedResult).toLayer - for { - response <- applyRoutes(request).provide(mockService) - _ <- response.body.asString - } yield assertTrue(true) - } - - val getSingleProjectSpec: Spec[Any, Serializable] = suite("get a single project by identifier")( - test("get a project by IRI") { - val iri = "http://rdfh.ch/projects/0001" - val identifier = TestDataFactory.projectIriIdentifier(iri) - val request = Request.get(url = URL(basePathProjectsIri / encode(iri))) - val mockService: ULayer[ProjectADMRestService] = ProjectADMRestServiceMock - .GetSingleProject( - assertion = Assertion.equalTo(identifier), - result = Expectation.valueF[ProjectIdentifierADM, ProjectGetResponseADM](id => - ProjectGetResponseADM(getProjectADM(ProjectIdentifierADM.getId(id))) - ) - ) - .toLayer - for { - response <- applyRoutes(request).provide(mockService) - body <- response.body.asString - } yield assertTrue(body.contains(iri)) - }, - test("return a BadRequest Exception if project IRI is invalid") { - val iri = "http://rdfh.ch/project/0001" - val request = Request.get(url = URL(basePathProjectsIri / encode(iri))) - - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - bodyAsString <- response.body.asString - } yield assertTrue( - response.status == Status.BadRequest, - bodyAsString == """{"error":"dsp.errors.BadRequestException: Project IRI is invalid."}""" - ) - }, - test("get a project by shortname") { - val shortname = "someProject" - val identifier = TestDataFactory.projectShortnameIdentifier(shortname) - val request = Request.get(url = URL(basePathProjectsShortname / shortname)) - val mockService: ULayer[ProjectADMRestService] = ProjectADMRestServiceMock - .GetSingleProject( - assertion = Assertion.equalTo(identifier), - result = Expectation.valueF[ProjectIdentifierADM, ProjectGetResponseADM](id => - ProjectGetResponseADM(getProjectADM(ProjectIdentifierADM.getId(id))) - ) - ) - .toLayer - for { - response <- applyRoutes(request).provide(mockService) - body <- response.body.asString - } yield assertTrue(body.contains(shortname)) - }, - test("return a BadRequest Exception if shortname is invalid") { - val shortname = "short name" - val request = Request.get(url = URL(basePathProjectsShortname / shortname)) - - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - bodyAsString <- response.body.asString - } yield assertTrue( - response.status == Status.BadRequest, - bodyAsString == """{"error":"dsp.errors.BadRequestException: Shortname is invalid: short name"}""" - ) - }, - test("get a project by shortcode") { - val shortcode = "0001" - val identifier = TestDataFactory.projectShortcodeIdentifier(shortcode) - val request = Request.get(url = URL(basePathProjectsShortcode / shortcode)) - val mockService: ULayer[ProjectADMRestService] = ProjectADMRestServiceMock - .GetSingleProject( - assertion = Assertion.equalTo(identifier), - result = Expectation.valueF[ProjectIdentifierADM, ProjectGetResponseADM](id => - ProjectGetResponseADM(getProjectADM(ProjectIdentifierADM.getId(id))) - ) - ) - .toLayer - for { - response <- applyRoutes(request).provide(mockService) - body <- response.body.asString - } yield assertTrue(body.contains(shortcode)) - }, - test("return a BadRequest Exception if shortcode is invalid") { - val shortcode = "XY" - val request = Request.get(url = URL(basePathProjectsShortcode / shortcode)) - - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - bodyAsString <- response.body.asString - } yield assertTrue( - response.status == Status.BadRequest, - bodyAsString == """{"error":"dsp.errors.BadRequestException: Shortcode is invalid: XY"}""" - ) - } - ) - - val createProjectSpec: Spec[Any, Serializable] = suite("create a project")( - test("successfully create a project") { - val projectCreatePayloadString = - """|{ - | "shortname": "newproject", - | "shortcode": "3333", - | "longname": "project longname", - | "description": [{"value": "project description", "language": "en"}], - | "keywords": ["test project"], - | "status": true, - | "selfjoin": false - |} - |""".stripMargin - val body = Body.fromString(projectCreatePayloadString) - val request = Request.post(url = URL(basePathProjects), body = body) - val user = KnoraSystemInstances.Users.SystemUser - - val shortname = TestDataFactory.projectShortname("newproject") - val shortcode = TestDataFactory.projectShortcode("3333") - val longname = TestDataFactory.projectName("project longname") - val description = TestDataFactory.projectDescription(Seq(V2.StringLiteralV2("project description", Some("en")))) - val keywords = TestDataFactory.projectKeywords(Seq("test project")) - val status = TestDataFactory.projectStatus(true) - val selfJoin = TestDataFactory.projectSelfJoin(false) - - val projectCreatePayload = ProjectCreateRequest( - id = None, - shortname = shortname, - shortcode = shortcode, - longname = Some(longname), - description = description, - keywords = keywords, - logo = None, - status = status, - selfjoin = selfJoin - ) - - val expectedResult = Expectation.value[ProjectOperationResponseADM](ProjectOperationResponseADM(getProjectADM())) - val mockService = ProjectADMRestServiceMock - .CreateProject( - assertion = Assertion.equalTo((projectCreatePayload, user)), - result = expectedResult - ) - .toLayer - for { - _ <- applyRoutes(request).provide(mockService) - } yield assertTrue(true) - }, - test("return a BadRequest Exception if input (payload) is invalid (wrong attribute)") { - val projectCreatePayloadString = - """|{ - | "shortname": "new project", - | "shortcode": "3333", - | "longname": "project longname", - | "description": [{"value": "project description", "language": "en"}], - | "keywords": ["test project"], - | "status": true, - | "selfjoin": false - |} - |""".stripMargin - val body = Body.fromString(projectCreatePayloadString) - val request = Request.post(url = URL(basePathProjects), body = body) - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - bodyAsString <- response.body.asString - } yield assertTrue( - response.status == Status.BadRequest, - bodyAsString == """{"error":"dsp.errors.BadRequestException: .shortname(Shortname is invalid: new project)"}""" - ) - }, - test("return a BadRequest Exception if input (syntax) is invalid") { - val projectCreatePayloadString = - """|{ - | "shortname": "newproject", - | "shortcode": "3333" - | "longname": "project longname", - | "description": [{"value": "project description", "language": "en"}], - | "keywords": ["test project"], - | "status": true, - | "selfjoin": false - |} - |""".stripMargin - val body = Body.fromString(projectCreatePayloadString) - val request = Request.post(url = URL(basePathProjects), body = body) - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - } yield assertTrue(response.status == Status.BadRequest) - } - ) - - val deleteProjectSpec: Spec[Any, Serializable] = suite("delete a project")( - test("successfully delete a project by IRI") { - val iri = "http://rdfh.ch/projects/0001" - val projectIri = TestDataFactory.projectIri(iri) - val request = Request.delete(url = URL(basePathProjectsIri / encode(projectIri.value))) - val user = KnoraSystemInstances.Users.SystemUser - val expectedResult = Expectation.value[ProjectOperationResponseADM](ProjectOperationResponseADM(getProjectADM())) - val mockService: ULayer[ProjectADMRestService] = ProjectADMRestServiceMock - .DeleteProject( - assertion = Assertion.equalTo(IriIdentifier.from(projectIri), user), - result = expectedResult - ) - .toLayer - for { - _ <- applyRoutes(request).provide(mockService) - } yield assertTrue(true) - }, - test("return a BadRequest Exception if project IRI is invalid") { - val iri = "http://rdfh.ch/project/0001" - val request = Request.delete(url = URL(basePathProjectsIri / encode(iri))) - - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - bodyAsString <- response.body.asString - } yield assertTrue( - response.status == Status.BadRequest, - bodyAsString == """{"error":"dsp.errors.BadRequestException: Project IRI is invalid."}""" - ) - } - ) - - val updateProjectSpec: Spec[Any, Serializable] = suite("update a project")( - test("successfully update a project") { - val projectIri = TestDataFactory.projectIri("http://rdfh.ch/projects/0001") - val updatedShortname = TestDataFactory.projectShortname("usn") - val updatedLongname = TestDataFactory.projectName("updated project longname") - val updatedDescription = TestDataFactory.projectDescription(Seq(V2.StringLiteralV2("updated desc", Some("en")))) - val updatedKeywords = TestDataFactory.projectKeywords(Seq("updated", "keywords")) - val updatedLogo = TestDataFactory.projectLogo("../logo.png") - val projectStatus = TestDataFactory.projectStatus(true) - val selfJoin = TestDataFactory.projectSelfJoin(true) - - val projectUpdatePayload = ProjectUpdateRequest( - shortname = Some(updatedShortname), - longname = Some(updatedLongname), - description = Some(updatedDescription), - keywords = Some(updatedKeywords), - logo = Some(updatedLogo), - status = Some(projectStatus), - selfjoin = Some(selfJoin) - ) - - val projectUpdatePayloadString = - s"""|{ - | "shortname": "${updatedShortname.value}", - | "longname": "${updatedLongname.value}", - | "description": [{"value": "updated desc", "language": "en"}], - | "keywords": ["updated", "keywords"], - | "logo": "${updatedLogo.value}", - | "status": ${projectStatus.value}, - | "selfjoin": ${selfJoin.value} - |} - |""".stripMargin - - val body = Body.fromString(projectUpdatePayloadString) - val request = Request.put(url = URL(basePathProjectsIri / encode(projectIri.value)), body = body) - val user = KnoraSystemInstances.Users.SystemUser - - val expectedResult = Expectation.value[ProjectOperationResponseADM](ProjectOperationResponseADM(getProjectADM())) - val mockService = ProjectADMRestServiceMock - .UpdateProject( - assertion = Assertion.equalTo((IriIdentifier.from(projectIri), projectUpdatePayload, user)), - result = expectedResult - ) - .toLayer - for { - _ <- applyRoutes(request).provide(mockService) - } yield assertTrue(true) - }, - test("return a BadRequest Exception if input (shortname) is invalid") { - val projectIri = "http://rdfh.ch/projects/0001" - val projectUpdatePayloadString = """{"shortname": "invalid shortname"}""".stripMargin - val body = Body.fromString(projectUpdatePayloadString) - val request = Request.put(url = URL(basePathProjectsIri / encode(projectIri)), body = body) - - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - bodyAsString <- response.body.asString - } yield assertTrue( - response.status == Status.BadRequest, - bodyAsString == """{"error":"dsp.errors.BadRequestException: .shortname(Shortname is invalid: invalid shortname)"}""" - ) - }, - test("return a BadRequest Exception if input (syntax) is invalid") { - val projectIri = "http://rdfh.ch/projects/0001" - val projectUpdatePayloadString = """{"shortname":"usn" "longname":"updated longname"}""".stripMargin - val body = Body.fromString(projectUpdatePayloadString) - val request = Request.put(url = URL(basePathProjectsIri / encode(projectIri)), body = body) - - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - } yield assertTrue(response.status == Status.BadRequest) - }, - test("return a BadRequest Exception if project IRI is invalid") { - val projectIri = "http://rdfh.ch/project/0001" - val projectUpdatePayloadString = """{"shortname":"usn"}""".stripMargin - val body = Body.fromString(projectUpdatePayloadString) - val request = Request.put(url = URL(basePathProjectsIri / encode(projectIri)), body = body) - - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - bodyAsString <- response.body.asString - } yield assertTrue( - response.status == Status.BadRequest, - bodyAsString == """{"error":"dsp.errors.BadRequestException: Project IRI is invalid."}""" - ) - } - ) - - val getAllDataSpec: Spec[Any, Serializable] = suite("get all data")( - test("successfully get all data") { - val iri = "http://rdfh.ch/projects/0001" - val identifier = TestDataFactory.projectIriIdentifier(iri) - val user = KnoraSystemInstances.Users.SystemUser - val request = Request.get(url = URL(basePathProjectsIri / encode(iri) / "AllData")) - val path = file.Paths.get("getAllDataFile.trig") - val testFile = file.Files.createFile(path) - - val mockService: ULayer[ProjectADMRestService] = ProjectADMRestServiceMock - .GetAllProjectData( - assertion = Assertion.equalTo(identifier, user), - result = Expectation - .value[ProjectDataGetResponseADM](ProjectDataGetResponseADM(testFile)) - ) - .toLayer - for { - response <- applyRoutes(request).provide(mockService) - _ <- response.body.asString - } yield assertTrue(true) - }, - test("return a BadRequest Exception if project IRI is invalid") { - val iri = "http://rdfh.ch/project/0001" - val request = Request.get(url = URL(basePathProjectsIri / encode(iri) / "AllData")) - - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - bodyAsString <- response.body.asString - } yield assertTrue( - response.status == Status.BadRequest, - bodyAsString == """{"error":"dsp.errors.BadRequestException: Project IRI is invalid."}""" - ) - } - ) - - val getProjectMembersSpec: Spec[Any, Serializable] = suite("get all members of a project")( - test("get all members by project IRI") { - val iri = "http://rdfh.ch/projects/0001" - val identifier = TestDataFactory.projectIriIdentifier(iri) - val user = KnoraSystemInstances.Users.SystemUser - val request = Request.get(url = URL(basePathProjectsIri / encode(iri) / "members")) - val mockService: ULayer[ProjectADMRestService] = ProjectADMRestServiceMock - .GetProjectMembers( - assertion = Assertion.equalTo((identifier, user)), - result = Expectation.value[ProjectMembersGetResponseADM]( - ProjectMembersGetResponseADM(Seq.empty[UserADM]) - ) - ) - .toLayer - for { - response <- applyRoutes(request).provide(mockService) - body <- response.body.asString - } yield assertTrue(body == """{"members":[]}""") - }, - test("return a BadRequest Exception if project IRI is invalid") { - val iri = "http://rdfh.ch/project/0001" - val request = Request.get(url = URL(basePathProjectsIri / encode(iri) / "members")) - - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - bodyAsString <- response.body.asString - } yield assertTrue( - response.status == Status.BadRequest, - bodyAsString == """{"error":"dsp.errors.BadRequestException: Project IRI is invalid."}""" - ) - }, - test("get all members by project shortcode") { - val shortcode = "0001" - val identifier = TestDataFactory.projectShortcodeIdentifier(shortcode) - val user = KnoraSystemInstances.Users.SystemUser - val request = Request.get(url = URL(basePathProjectsShortcode / shortcode / "members")) - val mockService: ULayer[ProjectADMRestService] = ProjectADMRestServiceMock - .GetProjectMembers( - assertion = Assertion.equalTo((identifier, user)), - result = Expectation.value[ProjectMembersGetResponseADM]( - ProjectMembersGetResponseADM(Seq.empty[UserADM]) - ) - ) - .toLayer - for { - response <- applyRoutes(request).provide(mockService) - body <- response.body.asString - } yield assertTrue(body == """{"members":[]}""") - }, - test("return a BadRequest Exception if shortcode is invalid") { - val shortcode = "XY" - val request = Request.get(url = URL(basePathProjectsShortcode / shortcode / "members")) - - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - bodyAsString <- response.body.asString - } yield assertTrue( - response.status == Status.BadRequest, - bodyAsString == """{"error":"dsp.errors.BadRequestException: Shortcode is invalid: XY"}""" - ) - }, - test("get all members by project shortname") { - val shortname = "someProject" - val identifier = TestDataFactory.projectShortnameIdentifier(shortname) - val user = KnoraSystemInstances.Users.SystemUser - val request = Request.get(url = URL(basePathProjectsShortname / shortname / "members")) - val mockService: ULayer[ProjectADMRestService] = ProjectADMRestServiceMock - .GetProjectMembers( - assertion = Assertion.equalTo((identifier, user)), - result = Expectation.value[ProjectMembersGetResponseADM]( - ProjectMembersGetResponseADM(Seq.empty[UserADM]) - ) - ) - .toLayer - for { - response <- applyRoutes(request).provide(mockService) - body <- response.body.asString - } yield assertTrue(body == """{"members":[]}""") - }, - test("return a BadRequest Exception if shortname is invalid") { - val shortname = "short name" - val request = Request.get(url = URL(basePathProjectsShortname / shortname / "members")) - - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - bodyAsString <- response.body.asString - } yield assertTrue( - response.status == Status.BadRequest, - bodyAsString == """{"error":"dsp.errors.BadRequestException: Shortname is invalid: short name"}""" - ) - } - ) - - val getProjectAdminsSpec: Spec[Any, Serializable] = suite("get all project admins of a project")( - test("get all project admins by project IRI") { - val iri = "http://rdfh.ch/projects/0001" - val identifier = TestDataFactory.projectIriIdentifier(iri) - val user = KnoraSystemInstances.Users.SystemUser - val request = Request.get(url = URL(basePathProjectsIri / encode(iri) / "admin-members")) - val mockService: ULayer[ProjectADMRestService] = ProjectADMRestServiceMock - .GetProjectAdmins( - assertion = Assertion.equalTo((identifier, user)), - result = Expectation.value[ProjectAdminMembersGetResponseADM]( - ProjectAdminMembersGetResponseADM(Seq.empty[UserADM]) - ) - ) - .toLayer - for { - response <- applyRoutes(request).provide(mockService) - body <- response.body.asString - } yield assertTrue(body == """{"members":[]}""") - }, - test("return a BadRequest Exception if project IRI is invalid") { - val iri = "http://rdfh.ch/project/0001" - val request = Request.get(url = URL(basePathProjectsIri / encode(iri) / "admin-members")) - - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - bodyAsString <- response.body.asString - } yield assertTrue( - response.status == Status.BadRequest, - bodyAsString == """{"error":"dsp.errors.BadRequestException: Project IRI is invalid."}""" - ) - }, - test("get all project admins by project shortcode") { - val shortcode = "0001" - val identifier = TestDataFactory.projectShortcodeIdentifier(shortcode) - val user = KnoraSystemInstances.Users.SystemUser - val request = Request.get(url = URL(basePathProjectsShortcode / shortcode / "admin-members")) - val mockService: ULayer[ProjectADMRestService] = ProjectADMRestServiceMock - .GetProjectAdmins( - assertion = Assertion.equalTo((identifier, user)), - result = Expectation.value[ProjectAdminMembersGetResponseADM]( - ProjectAdminMembersGetResponseADM(Seq.empty[UserADM]) - ) - ) - .toLayer - for { - response <- applyRoutes(request).provide(mockService) - body <- response.body.asString - } yield assertTrue(body == """{"members":[]}""") - }, - test("return a BadRequest Exception if shortcode is invalid") { - val shortcode = "XY" - val request = Request.get(url = URL(basePathProjectsShortcode / shortcode / "admin-members")) - - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - bodyAsString <- response.body.asString - } yield assertTrue( - response.status == Status.BadRequest, - bodyAsString == """{"error":"dsp.errors.BadRequestException: Shortcode is invalid: XY"}""" - ) - }, - test("get all project admins by project shortname") { - val shortname = "someProject" - val identifier = TestDataFactory.projectShortnameIdentifier(shortname) - val user = KnoraSystemInstances.Users.SystemUser - val request = Request.get(url = URL(basePathProjectsShortname / shortname / "admin-members")) - val mockService: ULayer[ProjectADMRestService] = ProjectADMRestServiceMock - .GetProjectAdmins( - assertion = Assertion.equalTo((identifier, user)), - result = Expectation.value[ProjectAdminMembersGetResponseADM]( - ProjectAdminMembersGetResponseADM(Seq.empty[UserADM]) - ) - ) - .toLayer - for { - response <- applyRoutes(request).provide(mockService) - body <- response.body.asString - } yield assertTrue(body == """{"members":[]}""") - }, - test("return a BadRequest Exception if shortname is invalid") { - val shortname = "short name" - val request = Request.get(url = URL(basePathProjectsShortname / shortname / "admin-members")) - - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - bodyAsString <- response.body.asString - } yield assertTrue( - response.status == Status.BadRequest, - bodyAsString == """{"error":"dsp.errors.BadRequestException: Shortname is invalid: short name"}""" - ) - } - ) - - val getKeywordsSpec: Spec[Any, Serializable] = test("get keywords of all projects") { - val request = Request.get(url = URL(basePathProjects / "Keywords")) - val expectedResult = - Expectation.value[ProjectsKeywordsGetResponseADM](ProjectsKeywordsGetResponseADM(Seq.empty[String])) - val mockService = ProjectADMRestServiceMock.GetKeywords(expectedResult).toLayer - for { - response <- applyRoutes(request).provide(mockService) - _ <- response.body.asString - } yield assertCompletes - } - - val getKeywordsByProjectSpec: Spec[Any, Serializable] = suite("get all keywords of a specific project")( - test("successfully get keywords") { - val iri = "http://rdfh.ch/projects/0001" - val projectIri = TestDataFactory.projectIri(iri) - val request = Request.get(url = URL(basePathProjectsIri / encode(iri) / "Keywords")) - val mockService: ULayer[ProjectADMRestService] = ProjectADMRestServiceMock - .GetKeywordsByProjectIri( - assertion = Assertion.equalTo(projectIri), - result = Expectation.value[ProjectKeywordsGetResponseADM]( - ProjectKeywordsGetResponseADM(Seq.empty[String]) - ) - ) - .toLayer - for { - response <- applyRoutes(request).provide(mockService) - body <- response.body.asString - } yield assertTrue(body == """{"keywords":[]}""") - }, - test("return a BadRequest Exception if project IRI is invalid") { - val iri = "http://rdfh.ch/project/0001" - val request = Request.get(url = URL(basePathProjectsIri / encode(iri) / "Keywords")) - - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - bodyAsString <- response.body.asString - } yield assertTrue( - response.status == Status.BadRequest, - bodyAsString == """{"error":"dsp.errors.BadRequestException: Project IRI is invalid."}""" - ) - } - ) - - val getProjectRestrictedViewSettings: Spec[Any, Serializable] = - suite("get the restricted view settings of a project by project identifier")( - test("successfully get the settings by project IRI") { - val iri = "http://rdfh.ch/projects/0001" - val identifier = TestDataFactory.projectIriIdentifier(iri) - val settings = ProjectRestrictedViewSettingsADM(Some("!512,512"), Some("path_to_image")) - val request = Request.get(url = URL(basePathProjectsIri / encode(iri) / "RestrictedViewSettings")) - val mockService: ULayer[ProjectADMRestService] = ProjectADMRestServiceMock - .GetRestrictedViewSettings( - assertion = Assertion.equalTo(identifier), - result = Expectation.valueF[ProjectIdentifierADM, ProjectRestrictedViewSettingsGetResponseADM](_ => - ProjectRestrictedViewSettingsGetResponseADM(settings) - ) - ) - .toLayer - for { - response <- applyRoutes(request).provide(mockService) - body <- response.body.asString - } yield assertTrue(body.contains("!512,512")) - }, - test("return a BadRequest Exception if project IRI is invalid") { - val iri = "http://rdfh.ch/project/0001" - val request = Request.get(url = URL(basePathProjectsIri / encode(iri) / "RestrictedViewSettings")) - - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - bodyAsString <- response.body.asString - } yield assertTrue( - response.status == Status.BadRequest, - bodyAsString == """{"error":"dsp.errors.BadRequestException: Project IRI is invalid."}""" - ) - }, - test("successfully get the settings by shortname") { - val shortname = "someProject" - val identifier = TestDataFactory.projectShortnameIdentifier(shortname) - val settings = ProjectRestrictedViewSettingsADM(Some("!512,512"), Some("path_to_image")) - val request = Request.get(url = URL(basePathProjectsShortname / shortname / "RestrictedViewSettings")) - val mockService: ULayer[ProjectADMRestService] = ProjectADMRestServiceMock - .GetRestrictedViewSettings( - assertion = Assertion.equalTo(identifier), - result = Expectation.valueF[ProjectIdentifierADM, ProjectRestrictedViewSettingsGetResponseADM](_ => - ProjectRestrictedViewSettingsGetResponseADM(settings) - ) - ) - .toLayer - for { - response <- applyRoutes(request).provide(mockService) - body <- response.body.asString - } yield assertTrue(body.contains("!512,512")) - }, - test("return a BadRequest Exception if shortname is invalid") { - val shortname = "short name" - val request = Request.get(url = URL(basePathProjectsShortname / shortname / "RestrictedViewSettings")) - - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - bodyAsString <- response.body.asString - } yield assertTrue( - response.status == Status.BadRequest, - bodyAsString == """{"error":"dsp.errors.BadRequestException: Shortname is invalid: short name"}""" - ) - }, - test("successfully get the settings by shortcode") { - val shortcode = "0001" - val identifier = TestDataFactory.projectShortcodeIdentifier(shortcode) - val settings = ProjectRestrictedViewSettingsADM(Some("!512,512"), Some("path_to_image")) - val request = Request.get(url = URL(basePathProjectsShortcode / shortcode / "RestrictedViewSettings")) - val mockService: ULayer[ProjectADMRestService] = ProjectADMRestServiceMock - .GetRestrictedViewSettings( - assertion = Assertion.equalTo(identifier), - result = Expectation.valueF[ProjectIdentifierADM, ProjectRestrictedViewSettingsGetResponseADM](_ => - ProjectRestrictedViewSettingsGetResponseADM(settings) - ) - ) - .toLayer - for { - response <- applyRoutes(request).provide(mockService) - body <- response.body.asString - } yield assertTrue(body.contains("!512,512")) - }, - test("return a BadRequest Exception if shortcode is invalid") { - val shortcode = "XY" - val request = Request.get(url = URL(basePathProjectsShortcode / shortcode / "RestrictedViewSettings")) - - for { - response <- applyRoutes(request).provide(ProjectADMRestServiceMock.empty) - bodyAsString <- response.body.asString - } yield assertTrue( - response.status == Status.BadRequest, - bodyAsString == """{"error":"dsp.errors.BadRequestException: Shortcode is invalid: XY"}""" - ) - } - ) -} diff --git a/webapi/src/test/scala/org/knora/webapi/slice/resourceinfo/api/LiveRestResourceInfoServiceSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/resourceinfo/api/LiveRestResourceInfoServiceSpec.scala index 8ecac63934..a3fe034046 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/resourceinfo/api/LiveRestResourceInfoServiceSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/resourceinfo/api/LiveRestResourceInfoServiceSpec.scala @@ -5,7 +5,7 @@ package org.knora.webapi.slice.resourceinfo.api -import zio.http.model.HttpError.BadRequest +import zio.http.HttpError.BadRequest import zio.test.Assertion.equalTo import zio.test.Assertion.fails import zio.test._ diff --git a/webapi/src/test/scala/org/knora/webapi/slice/resourceinfo/api/ResourceInfoRouteSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/resourceinfo/api/ResourceInfoRouteSpec.scala index 148e47aa62..6afddcf0ae 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/resourceinfo/api/ResourceInfoRouteSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/resourceinfo/api/ResourceInfoRouteSpec.scala @@ -8,7 +8,6 @@ package org.knora.webapi.slice.resourceinfo.api import zio.Chunk import zio.ZIO import zio.http._ -import zio.http.model._ import zio.test.ZIOSpecDefault import zio.test._ @@ -29,14 +28,10 @@ object ResourceInfoRouteSpec extends ZIOSpecDefault { private val testResourceClass = "http://test-resource-class/" + randomUUID private val testProjectIri = "http://test-project/" + randomUUID - private val baseUrl = URL(!! / "v2" / "resources" / "info") + private val baseUrl = URL(Root / "v2" / "resources" / "info") private val projectHeader = Headers("x-knora-accept-project", testProjectIri) - private def sendRequest(req: Request) = - for { - route <- ZIO.service[ResourceInfoRoute].map(_.route) - response <- route(req) - } yield response + private def sendRequest(req: Request) = ZIO.serviceWithZIO[ResourceInfoRoute](_.route.runZIO(req)) def spec = suite("ResourceInfoRoute /v2/resources/info")( @@ -48,28 +43,28 @@ object ResourceInfoRouteSpec extends ZIOSpecDefault { }, test("given more than one resource class should respond with BadRequest") { val params = QueryParams(("resourceClass", Chunk(testResourceClass, "http://anotherResourceClass"))) - val url = baseUrl.setQueryParams(params) + val url = baseUrl.withQueryParams(params) val request = Request.get(url = url).setHeaders(projectHeader) for { response <- sendRequest(request) } yield assertTrue(response.status == Status.BadRequest) }, test("given no projectIri should respond with BadRequest") { - val url = baseUrl.setQueryParams(QueryParams(("resourceClass", testResourceClass))) + val url = baseUrl.withQueryParams(QueryParams(("resourceClass", testResourceClass))) val request = Request.get(url = url) for { response <- sendRequest(request) } yield assertTrue(response.status == Status.BadRequest) }, test("given all mandatory parameters should respond with OK") { - val url = baseUrl.setQueryParams(QueryParams(("resourceClass", testResourceClass))) + val url = baseUrl.withQueryParams(QueryParams(("resourceClass", testResourceClass))) val request = Request.get(url = url).setHeaders(headers = projectHeader) for { response <- sendRequest(request) } yield assertTrue(response.status == Status.Ok) }, test("given all parameters rest service should be called with default order") { - val url = baseUrl.setQueryParams(QueryParams(("resourceClass", testResourceClass))) + val url = baseUrl.withQueryParams(QueryParams(("resourceClass", testResourceClass))) val request = Request.get(url = url).setHeaders(projectHeader) for { expectedResourceClassIri <- IriConverter.asInternalIri(testResourceClass).map(_.value) @@ -85,7 +80,7 @@ object ResourceInfoRouteSpec extends ZIOSpecDefault { ) }, test("given all parameters rest service should be called with correct parameters") { - val url = baseUrl.setQueryParams( + val url = baseUrl.withQueryParams( QueryParams( ("resourceClass", testResourceClass), ("orderBy", "creationDate"), diff --git a/webapi/src/test/scala/org/knora/webapi/slice/resourceinfo/api/RestResourceInfoServiceSpy.scala b/webapi/src/test/scala/org/knora/webapi/slice/resourceinfo/api/RestResourceInfoServiceSpy.scala index 8d366d7cfc..faf2a9d89e 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/resourceinfo/api/RestResourceInfoServiceSpy.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/resourceinfo/api/RestResourceInfoServiceSpy.scala @@ -6,7 +6,7 @@ package org.knora.webapi.slice.resourceinfo.api import zio._ -import zio.http.model.HttpError +import zio.http.HttpError import org.knora.webapi.IRI import org.knora.webapi.slice.resourceinfo.api.RestResourceInfoServiceSpy.orderingKey @@ -35,7 +35,7 @@ object RestResourceInfoServiceSpy { val resourceClassKey = "resourceClass" val orderingKey = "ordering" def lastInvocation: ZIO[RestResourceInfoServiceSpy, Nothing, Map[String, Any]] = - ZIO.service[RestResourceInfoServiceSpy].flatMap(_.lastInvocation.get) + ZIO.serviceWithZIO[RestResourceInfoServiceSpy](_.lastInvocation.get) val layer: ZLayer[IriConverter with ResourceInfoRepoFake, Nothing, RestResourceInfoServiceSpy] = ZLayer.fromZIO { for {