From 9f98f7e1d3872b2ff273f783b4374adbfeae2b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Thu, 12 Oct 2023 11:49:48 +0200 Subject: [PATCH] refactor: Migrate secure admin/projects endpoints to Tapir (#2872) --- docs/03-endpoints/api-v2/authentication.md | 49 ++-- .../org/knora/webapi/core/LayersTest.scala | 8 +- .../e2e/admin/ProjectsADME2EZioHttpSpec.scala | 6 +- .../ProjectsMessagesADMSpec.scala | 19 -- .../admin/ProjectsResponderADMSpec.scala | 22 +- webapi/src/main/scala/dsp/errors/Errors.scala | 18 +- .../main/scala/dsp/valueobjects/Project.scala | 4 + .../org/knora/webapi/core/LayersLive.scala | 8 +- .../webapi/http/status/ApiStatusCodesV2.scala | 2 +- .../webapi/http/status/ApiStatusCodesZ.scala | 2 +- .../ProjectsMessagesADM.scala | 179 +------------ .../ProjectsPayloadsADM.scala | 88 ------- .../admin/ProjectsResponderADM.scala | 37 ++- .../org/knora/webapi/routing/ApiRoutes.scala | 37 +-- .../knora/webapi/routing/Authenticator.scala | 3 + .../knora/webapi/routing/BaseEndpoints.scala | 35 --- .../knora/webapi/routing/HandlerMapperF.scala | 41 --- .../routing/admin/ProjectsEndpoints.scala | 93 ------- .../admin/ProjectsEndpointsHandlerF.scala | 90 ------- .../routing/admin/ProjectsRouteADM.scala | 230 ----------------- .../webapi/routing/admin/ProjectsRouteZ.scala | 48 ++-- .../slice/admin/api/ProjectsEndpoints.scala | 202 +++++++++++++++ .../admin/api/ProjectsEndpointsHandler.scala | 237 ++++++++++++++++++ .../api/model/ProjectExportResponse.scala | 8 +- .../api/model/ProjectsEndpointsRequests.scala | 49 ++++ .../api/service/ProjectsADMRestService.scala | 179 +++++++------ .../slice/common/api/BaseEndpoints.scala | 93 +++++++ .../slice/common/api/HandlerMapper.scala | 63 +++++ .../common/api}/TapirToPekkoInterpreter.scala | 5 +- .../admin/ProjectADMRestServiceMock.scala | 62 +++-- .../admin/ProjectsResponderADMMock.scala | 10 +- .../admin/ProjectsServiceLiveSpec.scala | 39 +-- .../routing/admin/ProjectsRouteZSpec.scala | 11 +- 33 files changed, 945 insertions(+), 1032 deletions(-) delete mode 100644 webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsPayloadsADM.scala delete mode 100644 webapi/src/main/scala/org/knora/webapi/routing/BaseEndpoints.scala delete mode 100644 webapi/src/main/scala/org/knora/webapi/routing/HandlerMapperF.scala delete mode 100644 webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsEndpoints.scala delete mode 100644 webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsEndpointsHandlerF.scala delete mode 100644 webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpoints.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpointsHandler.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsEndpointsRequests.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/common/api/HandlerMapper.scala rename webapi/src/main/scala/org/knora/webapi/{routing => slice/common/api}/TapirToPekkoInterpreter.scala (94%) diff --git a/docs/03-endpoints/api-v2/authentication.md b/docs/03-endpoints/api-v2/authentication.md index 9ab00d4eda..1e48771452 100644 --- a/docs/03-endpoints/api-v2/authentication.md +++ b/docs/03-endpoints/api-v2/authentication.md @@ -5,48 +5,27 @@ # Authentication -Access to the DSP-API can for certain operations require a user to authenticate. -Authentication can be performed in two ways: +Certain routes are secured and require authentication. +When accessing any secured route we support three options for authentication: -1. By providing *password credentials*, which are a combination of a *identifier* and - *password*. The user *identifier* can be one of the following: - - the user's IRI, - - the user's Email, or - - the user's Username. +- **Preferred method**: For each request an [Access Token](#Access-Token-/-Login-and-Logout) is sent in the HTTP + authorization + header with the + [HTTP bearer scheme](https://tools.ietf.org/html/rfc6750#section-2.1). +- **Deprecated method**: For each request an [Access Token](#Access-Token-/-Login-and-Logout) is provided as a cookie in + the HTTP request. +- **Deprecated method**: [HTTP basic authentication](https://en.wikipedia.org/wiki/Basic_access_authentication), where + the username is the user's `email`. -2. By providing an *access token* +## Access Token / Login and Logout -## Submitting Password Credentials - -When accessing any route and password credentials would need to be sent, -we support two options to do so: - -- in the URL submitting the parameters `iri` / `email` / `username` and `password` - (e.g., ), and -- in the HTTP header ([HTTP basic - authentication](https://en.wikipedia.org/wiki/Basic_access_authentication)), where the - identifier can be the user's `email` (IRI and username not supported). - -When using Python's module `requests`, the credentials can simply be submitted as a tuple with -each request using the param `auth` ([python requests](http://docs.python-requests.org/en/master/user/authentication/#basic-authentication)). - -## Access Token / Session / Login and Logout - -A client can generate an *access token* by sending a POST request (e.g., `{"identifier_type":"identifier_value", +A client can obtain an *access token* by sending a POST request (e.g., `{"identifier_type":"identifier_value", "password":"password_value"}`) to the **/v2/authentication** route with *identifier* and *password* in the body. The `identifier_type` can be `iri`, `email`, or `username`. If the credentials are valid, a [JSON WEB Token](https://jwt.io) (JWT) will be sent back in the response (e.g., `{"token": "eyJ0eXAiOiJ..."}`). Additionally, for web browser clients a session cookie containing the JWT token is also created, containing `KnoraAuthentication=eyJ0eXAiOiJ...`. -When accessing any route, the *access token* would need to be supplied, we support three options to do so: - -- the session cookie, -- in the URL submitting the parameter `token` (e.g., ), and -- in the HTTP authorization header with the [HTTP bearer scheme](https://tools.ietf.org/html/rfc6750#section-2.1). - -If the token is successfully validated, then the user is deemed authenticated. - To **logout**, the client sends a DELETE request to the same route **/v2/authentication** and the *access token* in one of the three described ways. This will invalidate the access token, thus not allowing further request that would supply the invalidated token. @@ -58,5 +37,5 @@ supplied as URL parameters or HTTP authentication headers as described before. ## Usage Scenarios -1. Create token by logging-in, send token on each subsequent request, and logout when finished. -2. Send email/password credentials on every request. +1. Create token by logging-in, send token on each subsequent request, and logout when finished. +2. Send email/password credentials on every request. 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 5c70ea8c01..4e39a94a3a 100644 --- a/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala +++ b/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala @@ -29,13 +29,15 @@ 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.ProjectsEndpoints -import org.knora.webapi.routing.admin.ProjectsEndpointsHandlerF import org.knora.webapi.routing.admin.ProjectsRouteZ +import org.knora.webapi.slice.admin.api.ProjectsEndpoints +import org.knora.webapi.slice.admin.api.ProjectsEndpointsHandler import org.knora.webapi.slice.admin.api.service.ProjectADMRestService import org.knora.webapi.slice.admin.api.service.ProjectsADMRestServiceLive import org.knora.webapi.slice.admin.domain.service._ import org.knora.webapi.slice.admin.repo.service.KnoraProjectRepoLive +import org.knora.webapi.slice.common.api.BaseEndpoints +import org.knora.webapi.slice.common.api.HandlerMapperF import org.knora.webapi.slice.common.api.RestPermissionService import org.knora.webapi.slice.common.api.RestPermissionServiceLive import org.knora.webapi.slice.common.repo.service.PredicateObjectMapper @@ -174,7 +176,7 @@ object LayersTest { ProjectImportServiceLive.layer, ProjectsADMRestServiceLive.layer, ProjectsEndpoints.layer, - ProjectsEndpointsHandlerF.layer, + ProjectsEndpointsHandler.layer, ProjectsResponderADMLive.layer, ProjectsRouteZ.layer, QueryTraverser.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 632557ac84..524049f977 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 @@ -776,7 +776,7 @@ class ProjectsADME2EZioHttpSpec extends E2ESpec with ProjectsADMJsonProtocol { } } - if (baseApiUrl.contains("5555")) "used to set RestrictedViewSize by project IRI" should { + "used to set RestrictedViewSize by project IRI" should { "return requested value to be set with 200 Response Status" in { val encodedIri = URLEncoder.encode(SharedTestDataADM.imagesProject.id, "utf-8") val payload = """{"size":"pct:1"}""" @@ -823,9 +823,8 @@ class ProjectsADME2EZioHttpSpec extends E2ESpec with ProjectsADMJsonProtocol { assert(response.status === StatusCodes.Forbidden) } } - else "used to set RestrictedViewSize by project IRI" ignore () - if (baseApiUrl.contains("5555")) "used to set RestrictedViewSize by project Shortcode" should { + "used to set RestrictedViewSize by project Shortcode" should { "return requested value to be set with 200 Response Status" in { val shortcode = SharedTestDataADM.imagesProject.shortcode val payload = """{"size":"pct:1"}""" @@ -872,6 +871,5 @@ class ProjectsADME2EZioHttpSpec extends E2ESpec with ProjectsADMJsonProtocol { assert(response.status === StatusCodes.Forbidden) } } - else "used to set RestrictedViewSize by project Shortcode" ignore () } } diff --git a/integration/src/test/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADMSpec.scala b/integration/src/test/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADMSpec.scala index 19df2c938d..d91a33a590 100644 --- a/integration/src/test/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADMSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADMSpec.scala @@ -9,7 +9,6 @@ import dsp.errors.BadRequestException import dsp.errors.OntologyConstraintException import dsp.valueobjects.V2 import org.knora.webapi._ -import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM._ import org.knora.webapi.sharedtestdata.SharedTestDataADM @@ -17,24 +16,6 @@ import org.knora.webapi.sharedtestdata.SharedTestDataADM * This spec is used to test subclasses of the [[ProjectsResponderRequestADM]] trait. */ class ProjectsMessagesADMSpec extends CoreSpec { - private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - - "The ChangeProjectApiRequestADM case class" should { - "return a 'BadRequest' when everything is 'None" in { - assertThrows[BadRequestException]( - ChangeProjectApiRequestADM( - shortname = None, - longname = None, - description = None, - keywords = None, - logo = None, - status = None, - selfjoin = None - ) - ) - } - } - "The ProjectADM case class" should { "return a 'OntologyConstraintException' when project description is not supplied" in { assertThrows[OntologyConstraintException]( diff --git a/integration/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMSpec.scala b/integration/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMSpec.scala index 22bd9ad013..14ac4b2d56 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMSpec.scala @@ -26,6 +26,8 @@ import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentif import org.knora.webapi.messages.admin.responder.projectsmessages._ import org.knora.webapi.messages.admin.responder.usersmessages.UserInformationTypeADM import org.knora.webapi.sharedtestdata.SharedTestDataADM +import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectCreateRequest +import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectUpdateRequest import org.knora.webapi.util.MutableTestIri import pekko.actor.Status.Failure @@ -164,7 +166,7 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender { "CREATE a project and return the project info if the supplied shortname is unique" in { val shortcode = "111c" appActor ! ProjectCreateRequestADM( - createRequest = ProjectCreatePayloadADM( + createRequest = ProjectCreateRequest( shortname = Shortname.make("newproject").fold(error => throw error.head, value => value), shortcode = Shortcode.make(shortcode).fold(error => throw error.head, value => value), // lower case longname = Name.make(Some("project longname")).fold(error => throw error.head, value => value), @@ -259,7 +261,7 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender { "CREATE a project and return the project info if the supplied shortname and shortcode is unique" in { appActor ! ProjectCreateRequestADM( - createRequest = ProjectCreatePayloadADM( + createRequest = ProjectCreateRequest( shortname = Shortname.make("newproject2").fold(error => throw error.head, value => value), shortcode = Shortcode.make("1112").fold(error => throw error.head, value => value), // lower case longname = Some(Name.make("project longname").fold(error => throw error.head, value => value)), @@ -291,7 +293,7 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender { val descriptionWithSpecialCharacter = "project \\\"description\\\"" val keywordWithSpecialCharacter = "new \\\"keyword\\\"" appActor ! ProjectCreateRequestADM( - createRequest = ProjectCreatePayloadADM( + createRequest = ProjectCreateRequest( shortname = Shortname.make("project_with_char").fold(error => throw error.head, value => value), shortcode = Shortcode.make("1312").fold(error => throw error.head, value => value), // lower case longname = Name.make(Some(longnameWithSpecialCharacter)).fold(error => throw error.head, value => value), @@ -323,7 +325,7 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender { "return a 'DuplicateValueException' during creation if the supplied project shortname is not unique" in { appActor ! ProjectCreateRequestADM( - createRequest = ProjectCreatePayloadADM( + createRequest = ProjectCreateRequest( shortname = Shortname.make("newproject").fold(error => throw error.head, value => value), shortcode = Shortcode.make("111C").fold(error => throw error.head, value => value), // lower case longname = Name.make(Some("project longname")).fold(error => throw error.head, value => value), @@ -343,7 +345,7 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender { "return a 'DuplicateValueException' during creation if the supplied project shortname is unique but the shortcode is not" in { appActor ! ProjectCreateRequestADM( - createRequest = ProjectCreatePayloadADM( + createRequest = ProjectCreateRequest( shortname = Shortname.make("newproject3").fold(error => throw error.head, value => value), shortcode = Shortcode.make("111C").fold(error => throw error.head, value => value), // lower case longname = Name.make(Some("project longname")).fold(error => throw error.head, value => value), @@ -374,7 +376,7 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender { appActor ! ProjectChangeRequestADM( projectIri = iri, - projectUpdatePayload = ProjectUpdatePayloadADM( + projectUpdatePayload = ProjectUpdateRequest( shortname = None, longname = Some(updatedLongname), description = Some(updatedDescription), @@ -409,7 +411,7 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender { val iri = ITTestDataFactory.projectIri(notExistingProjectButValidProjectIri) appActor ! ProjectChangeRequestADM( projectIri = iri, - projectUpdatePayload = ProjectUpdatePayloadADM(longname = Some(longname)), + projectUpdatePayload = ProjectUpdateRequest(longname = Some(longname)), SharedTestDataADM.rootUser, UUID.randomUUID() ) @@ -421,12 +423,6 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender { ) ) } - - "return 'BadRequest' if nothing would be changed during the update" in { - - an[BadRequestException] should be thrownBy ChangeProjectApiRequestADM(None, None, None, None, None, None, None) - - } } "used to query members" should { diff --git a/webapi/src/main/scala/dsp/errors/Errors.scala b/webapi/src/main/scala/dsp/errors/Errors.scala index 28e54a582f..1b5149c653 100644 --- a/webapi/src/main/scala/dsp/errors/Errors.scala +++ b/webapi/src/main/scala/dsp/errors/Errors.scala @@ -103,6 +103,10 @@ object BadRequestException { */ case class BadCredentialsException(message: String) extends RequestRejectedException(message) +object BadCredentialsException { + implicit val codec: JsonCodec[BadCredentialsException] = DeriveJsonCodec.gen[BadCredentialsException] +} + /** * An exception indicating that a user has made a request for which the user lacks the necessary permission. * @@ -110,6 +114,10 @@ case class BadCredentialsException(message: String) extends RequestRejectedExcep */ case class ForbiddenException(message: String) extends RequestRejectedException(message) +object ForbiddenException { + implicit val codec: JsonCodec[ForbiddenException] = DeriveJsonCodec.gen[ForbiddenException] +} + /** * An exception indicating that the requested data was not found. * @@ -130,6 +138,10 @@ object NotFoundException { case class DuplicateValueException(message: String = "Duplicate values are not permitted") extends RequestRejectedException(message) +object DuplicateValueException { + implicit val codec: JsonCodec[DuplicateValueException] = DeriveJsonCodec.gen[DuplicateValueException] +} + /** * An exception indicating that a requested update is not allowed because it would violate an ontology constraint, * e.g. an `knora-base:objectClassConstraint` or an OWL cardinality restriction. @@ -189,7 +201,11 @@ case class InvalidRdfException(msg: String, cause: Throwable = null) extends Req * @param msg a description of the error. * @param cause the cause for the error */ -case class ValidationException(msg: String, cause: Throwable = null) extends RequestRejectedException(msg, cause) +case class ValidationException(msg: String) extends RequestRejectedException(msg) + +object ValidationException { + implicit val codec: JsonCodec[ValidationException] = DeriveJsonCodec.gen[ValidationException] +} /** * An abstract class for exceptions indicating that something went wrong and it's not the client's fault. diff --git a/webapi/src/main/scala/dsp/valueobjects/Project.scala b/webapi/src/main/scala/dsp/valueobjects/Project.scala index d2a149e615..6d6ba1a489 100644 --- a/webapi/src/main/scala/dsp/valueobjects/Project.scala +++ b/webapi/src/main/scala/dsp/valueobjects/Project.scala @@ -222,6 +222,10 @@ object Project { */ sealed abstract case class ProjectStatus private (value: Boolean) object ProjectStatus { self => + + val deleted = new ProjectStatus(false) {} + val active = new ProjectStatus(true) {} + implicit val decoder: JsonDecoder[ProjectStatus] = JsonDecoder[Boolean].mapOrFail { case value => ProjectStatus.make(value).toEitherWith(e => e.head.getMessage()) } 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 c30f2e8c28..2585dfca5c 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -29,13 +29,15 @@ 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.ProjectsEndpoints -import org.knora.webapi.routing.admin.ProjectsEndpointsHandlerF import org.knora.webapi.routing.admin.ProjectsRouteZ +import org.knora.webapi.slice.admin.api.ProjectsEndpoints +import org.knora.webapi.slice.admin.api.ProjectsEndpointsHandler import org.knora.webapi.slice.admin.api.service.ProjectADMRestService import org.knora.webapi.slice.admin.api.service.ProjectsADMRestServiceLive import org.knora.webapi.slice.admin.domain.service._ import org.knora.webapi.slice.admin.repo.service.KnoraProjectRepoLive +import org.knora.webapi.slice.common.api.BaseEndpoints +import org.knora.webapi.slice.common.api.HandlerMapperF import org.knora.webapi.slice.common.api.RestPermissionService import org.knora.webapi.slice.common.api.RestPermissionServiceLive import org.knora.webapi.slice.common.repo.service.PredicateObjectMapper @@ -176,7 +178,7 @@ object LayersLive { ProjectImportServiceLive.layer, ProjectsADMRestServiceLive.layer, ProjectsEndpoints.layer, - ProjectsEndpointsHandlerF.layer, + ProjectsEndpointsHandler.layer, ProjectsResponderADMLive.layer, ProjectsRouteZ.layer, QueryTraverser.layer, diff --git a/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesV2.scala b/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesV2.scala index 890bd1124b..bba8160036 100644 --- a/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesV2.scala @@ -34,7 +34,7 @@ object ApiStatusCodesV2 { case OntologyConstraintException(_) => StatusCodes.BadRequest case EditConflictException(_) => StatusCodes.Conflict case BadRequestException(_) => StatusCodes.BadRequest - case ValidationException(_, _) => StatusCodes.BadRequest + case ValidationException(_) => StatusCodes.BadRequest case RequestRejectedException(_) => StatusCodes.BadRequest // RequestRejectedException must be the last one in this group 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 cd328287fe..3c642878c0 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 @@ -33,7 +33,7 @@ object ApiStatusCodesZ { case OntologyConstraintException(_) => Status.BadRequest case EditConflictException(_) => Status.Conflict case BadRequestException(_) => Status.BadRequest - case ValidationException(_, _) => Status.BadRequest + case ValidationException(_) => Status.BadRequest case RequestRejectedException(_) => Status.BadRequest // RequestRejectedException must be the last one in this group diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala index 5ba7027356..bb133d2997 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala @@ -17,7 +17,6 @@ import zio.prelude.Validation import java.util.UUID -import dsp.errors.BadRequestException import dsp.errors.OntologyConstraintException import dsp.errors.ValidationException import dsp.valueobjects.Iri @@ -33,135 +32,11 @@ import org.knora.webapi.messages.admin.responder.KnoraResponseADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM._ import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.store.triplestoremessages.TriplestoreJsonProtocol +import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectCreateRequest +import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectUpdateRequest import pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport -////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// API requests - -/** - * Represents an API request payload that asks the Knora API server to create a new project. - * - * @param id the optional IRI of the project to be created. - * @param shortname the shortname of the project to be created (unique). - * @param shortcode the shortcode of the project to be creates (unique) - * @param longname the longname of the project to be created. - * @param description the description of the project to be created. - * @param keywords the keywords of the project to be created (optional). - * @param logo the logo of the project to be created. - * @param status the status of the project to be created (active = true, inactive = false). - * @param selfjoin the status of self-join of the project to be created. - */ -case class CreateProjectApiRequestADM( - id: Option[IRI] = None, - shortname: String, - shortcode: String, - longname: Option[String], - description: Seq[V2.StringLiteralV2], - keywords: Seq[String], - logo: Option[String], - status: Boolean, - selfjoin: Boolean -) extends ProjectsADMJsonProtocol { - /* Convert to Json */ - def toJsValue: JsValue = createProjectApiRequestADMFormat.write(this) -} - -/** - * Represents an API request payload that asks the Knora API server to update an existing project. - * - * @param shortname the new project's shortname. - * @param longname the new project's longname. - * @param description the new project's description. - * @param keywords the new project's keywords. - * @param logo the new project's logo. - * @param status the new project's status. - * @param selfjoin the new project's self-join status. - */ -case class ChangeProjectApiRequestADM( - shortname: Option[String] = None, - longname: Option[String] = None, - description: Option[Seq[V2.StringLiteralV2]] = None, - keywords: Option[Seq[String]] = None, - logo: Option[String] = None, - status: Option[Boolean] = None, - selfjoin: Option[Boolean] = None -) extends ProjectsADMJsonProtocol { - implicit protected val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - - val parametersCount: Int = List( - shortname, - longname, - description, - keywords, - logo, - status, - selfjoin - ).flatten.size - - // something needs to be sent, i.e. everything 'None' is not allowed - if (parametersCount == 0) throw BadRequestException("No data sent in API request.") - - def toJsValue: JsValue = changeProjectApiRequestADMFormat.write(this) - - /* validates and escapes the given values.*/ - def validateAndEscape: ChangeProjectApiRequestADM = { - - val validatedShortname: Option[String] = - shortname.map(v => - validateAndEscapeProjectShortname(v) - .getOrElse(throw BadRequestException(s"The supplied short name: '$v' is not valid.")) - ) - - val validatedLongName: Option[String] = - longname.map(l => - Iri - .toSparqlEncodedString(l) - .getOrElse(throw BadRequestException(s"The supplied longname: '$l' is not valid.")) - ) - - val validatedLogo: Option[String] = - logo.map(l => - Iri - .toSparqlEncodedString(l) - .getOrElse(throw BadRequestException(s"The supplied logo: '$l' is not valid.")) - ) - - val validatedDescriptions: Option[Seq[V2.StringLiteralV2]] = description match { - case Some(descriptions: Seq[V2.StringLiteralV2]) => - val escapedDescriptions = descriptions.map { des => - val escapedValue = - Iri - .toSparqlEncodedString(des.value) - .getOrElse(throw BadRequestException(s"The supplied description: '${des.value}' is not valid.")) - V2.StringLiteralV2(value = escapedValue, language = des.language) - } - Some(escapedDescriptions) - case None => None - } - - val validatedKeywords: Option[Seq[String]] = keywords match { - case Some(givenKeywords: Seq[String]) => - val escapedKeywords = givenKeywords.map(keyword => - Iri - .toSparqlEncodedString(keyword) - .getOrElse( - throw BadRequestException(s"The supplied keyword: '$keyword' is not valid.") - ) - ) - Some(escapedKeywords) - case None => None - } - copy( - shortname = validatedShortname, - longname = validatedLongName, - description = validatedDescriptions, - keywords = validatedKeywords, - logo = validatedLogo - ) - } -} - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Messages @@ -253,12 +128,12 @@ case class ProjectRestrictedViewSettingsGetRequestADM( /** * Requests the creation of a new project. * - * @param createRequest the [[ProjectCreatePayloadADM]] information for the creation of a new project. - * @param requestingUser the user making the request. - * @param apiRequestID the ID of the API request. + * @param createRequest the [[ProjectCreateRequest]] information for the creation of a new project. + * @param requestingUser the user making the request. + * @param apiRequestID the ID of the API request. */ case class ProjectCreateRequestADM( - createRequest: ProjectCreatePayloadADM, + createRequest: ProjectCreateRequest, requestingUser: UserADM, apiRequestID: UUID ) extends ProjectsResponderRequestADM @@ -267,13 +142,13 @@ case class ProjectCreateRequestADM( * Requests updating an existing project. * * @param projectIri the IRI of the project to be updated. - * @param projectUpdatePayload the [[ProjectUpdatePayloadADM]] + * @param projectUpdatePayload the [[ProjectUpdateRequest]] * @param requestingUser the user making the request. * @param apiRequestID the ID of the API request. */ case class ProjectChangeRequestADM( projectIri: ProjectIri, - projectUpdatePayload: ProjectUpdatePayloadADM, + projectUpdatePayload: ProjectUpdateRequest, requestingUser: UserADM, apiRequestID: UUID ) extends ProjectsResponderRequestADM @@ -488,10 +363,11 @@ object ProjectIdentifierADM { */ final case class IriIdentifier(value: ProjectIri) extends ProjectIdentifierADM object IriIdentifier { + + def from(projectIri: ProjectIri): IriIdentifier = IriIdentifier(projectIri) + def fromString(value: String): Validation[ValidationException, IriIdentifier] = - ProjectIri.make(value).map { - IriIdentifier(_) - } + ProjectIri.make(value).map(IriIdentifier(_)) } /** @@ -501,6 +377,7 @@ object ProjectIdentifierADM { */ final case class ShortcodeIdentifier(value: Shortcode) extends ProjectIdentifierADM object ShortcodeIdentifier { + def from(shortcode: Shortcode): ShortcodeIdentifier = ShortcodeIdentifier(shortcode) def fromString(value: String): Validation[ValidationException, ShortcodeIdentifier] = Shortcode.make(value).map { ShortcodeIdentifier(_) @@ -582,36 +459,6 @@ trait ProjectsADMJsonProtocol extends SprayJsonSupport with DefaultJsonProtocol implicit val projectMembersGetResponseADMFormat: RootJsonFormat[ProjectMembersGetResponseADM] = rootFormat( lazyFormat(jsonFormat(ProjectMembersGetResponseADM, "members")) ) - implicit val createProjectApiRequestADMFormat: RootJsonFormat[CreateProjectApiRequestADM] = rootFormat( - lazyFormat( - jsonFormat( - CreateProjectApiRequestADM, - "id", - "shortname", - "shortcode", - "longname", - "description", - "keywords", - "logo", - "status", - "selfjoin" - ) - ) - ) - implicit val changeProjectApiRequestADMFormat: RootJsonFormat[ChangeProjectApiRequestADM] = rootFormat( - lazyFormat( - jsonFormat( - ChangeProjectApiRequestADM, - "shortname", - "longname", - "description", - "keywords", - "logo", - "status", - "selfjoin" - ) - ) - ) implicit val projectsKeywordsGetResponseADMFormat: RootJsonFormat[ProjectsKeywordsGetResponseADM] = jsonFormat(ProjectsKeywordsGetResponseADM, "keywords") implicit val projectKeywordsGetResponseADMFormat: RootJsonFormat[ProjectKeywordsGetResponseADM] = diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsPayloadsADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsPayloadsADM.scala deleted file mode 100644 index 132026f947..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsPayloadsADM.scala +++ /dev/null @@ -1,88 +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.messages.admin.responder.projectsmessages - -import zio.json._ -import zio.prelude.Validation - -import dsp.errors.ValidationException -import dsp.valueobjects.Iri.ProjectIri -import dsp.valueobjects.Project._ - -/** - * Project creation payload - */ -final case class ProjectCreatePayloadADM( - id: Option[ProjectIri] = None, - shortname: Shortname, - shortcode: Shortcode, - longname: Option[Name] = None, - description: ProjectDescription, - keywords: Keywords, - logo: Option[Logo] = None, - status: ProjectStatus, - selfjoin: ProjectSelfJoin -) - -object ProjectCreatePayloadADM { - - implicit val codec: JsonCodec[ProjectCreatePayloadADM] = DeriveJsonCodec.gen[ProjectCreatePayloadADM] - - def make(apiRequest: CreateProjectApiRequestADM): Validation[Throwable, ProjectCreatePayloadADM] = { - val id: Validation[Throwable, Option[ProjectIri]] = ProjectIri.make(apiRequest.id) - val shortname: Validation[Throwable, Shortname] = Shortname.make(apiRequest.shortname) - val shortcode: Validation[Throwable, Shortcode] = Shortcode.make(apiRequest.shortcode) - val longname: Validation[Throwable, Option[Name]] = Name.make(apiRequest.longname) - val description: Validation[Throwable, ProjectDescription] = ProjectDescription.make(apiRequest.description) - val keywords: Validation[Throwable, Keywords] = Keywords.make(apiRequest.keywords) - val logo: Validation[Throwable, Option[Logo]] = Logo.make(apiRequest.logo) - val status: Validation[Throwable, ProjectStatus] = ProjectStatus.make(apiRequest.status) - val selfjoin: Validation[Throwable, ProjectSelfJoin] = ProjectSelfJoin.make(apiRequest.selfjoin) - Validation.validateWith(id, shortname, shortcode, longname, description, keywords, logo, status, selfjoin)( - ProjectCreatePayloadADM.apply - ) - } -} - -/** - * Project update payload - */ -final case class ProjectUpdatePayloadADM( - shortname: Option[Shortname] = None, - longname: Option[Name] = None, - description: Option[ProjectDescription] = None, - keywords: Option[Keywords] = None, - logo: Option[Logo] = None, - status: Option[ProjectStatus] = None, - selfjoin: Option[ProjectSelfJoin] = None -) - -object ProjectUpdatePayloadADM { - - implicit val codec: JsonCodec[ProjectUpdatePayloadADM] = DeriveJsonCodec.gen[ProjectUpdatePayloadADM] - - def make(apiRequest: ChangeProjectApiRequestADM): Validation[Throwable, ProjectUpdatePayloadADM] = { - val shortname: Validation[ValidationException, Option[Shortname]] = Shortname.make(apiRequest.shortname) - val longname: Validation[Throwable, Option[Name]] = Name.make(apiRequest.longname) - val description: Validation[Throwable, Option[ProjectDescription]] = - ProjectDescription.make(apiRequest.description) - val keywords: Validation[Throwable, Option[Keywords]] = Keywords.make(apiRequest.keywords) - val logo: Validation[Throwable, Option[Logo]] = Logo.make(apiRequest.logo) - val status: Validation[Throwable, Option[ProjectStatus]] = ProjectStatus.make(apiRequest.status) - val selfjoin: Validation[Throwable, Option[ProjectSelfJoin]] = ProjectSelfJoin.make(apiRequest.selfjoin) - - Validation.validateWith(shortname, longname, description, keywords, logo, status, selfjoin)( - ProjectUpdatePayloadADM.apply - ) - } -} - -final case class ProjectSetRestrictedViewSizePayload(size: String) - -object ProjectSetRestrictedViewSizePayload { - implicit val codec: JsonCodec[ProjectSetRestrictedViewSizePayload] = - DeriveJsonCodec.gen[ProjectSetRestrictedViewSizePayload] -} diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala index 1205974b6b..5f13feab0c 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala @@ -38,6 +38,8 @@ import org.knora.webapi.responders.IriLocker import org.knora.webapi.responders.IriService import org.knora.webapi.responders.Responder import org.knora.webapi.slice.admin.AdminConstants +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.domain.service.ProjectADMService import org.knora.webapi.store.cache.settings.CacheServiceSettings import org.knora.webapi.store.triplestore.api.TriplestoreService @@ -135,7 +137,7 @@ trait ProjectsResponderADM { /** * Creates a project. * - * @param createPayload the new project's information. + * @param projectCreate the new project's information. * @param requestingUser the user that is making the request. * @param apiRequestID the unique api request ID. * @return A [[ProjectOperationResponseADM]]. @@ -147,7 +149,7 @@ trait ProjectsResponderADM { * [[BadRequestException]] In the case when the shortcode is invalid. */ def projectCreateRequestADM( - createPayload: ProjectCreatePayloadADM, + projectCreate: ProjectCreateRequest, requestingUser: UserADM, apiRequestID: UUID ): Task[ProjectOperationResponseADM] @@ -156,7 +158,7 @@ trait ProjectsResponderADM { * Update project's basic information. * * @param projectIri the IRI of the project. - * @param updatePayload the update payload. + * @param projectUpdate the update payload. * @param user the user making the request. * @param apiRequestID the unique api request ID. * @return A [[ProjectOperationResponseADM]]. @@ -165,7 +167,7 @@ trait ProjectsResponderADM { */ def changeBasicInformationRequestADM( projectIri: Iri.ProjectIri, - updatePayload: ProjectUpdatePayloadADM, + projectUpdate: ProjectUpdateRequest, user: UserADM, apiRequestID: UUID ): Task[ProjectOperationResponseADM] @@ -456,7 +458,7 @@ final case class ProjectsResponderADMLive( * Update project's basic information. * * @param projectIri the IRI of the project. - * @param updatePayload the update payload. + * @param projectUpdate the update payload. * @param user the user making the request. * @param apiRequestID the unique api request ID. * @return A [[ProjectOperationResponseADM]]. @@ -465,7 +467,7 @@ final case class ProjectsResponderADMLive( */ override def changeBasicInformationRequestADM( projectIri: Iri.ProjectIri, - updatePayload: ProjectUpdatePayloadADM, + projectUpdate: ProjectUpdateRequest, user: UserADM, apiRequestID: UUID ): Task[ProjectOperationResponseADM] = { @@ -475,20 +477,17 @@ final case class ProjectsResponderADMLive( */ def changeProjectTask( projectIri: Iri.ProjectIri, - projectUpdatePayload: ProjectUpdatePayloadADM, + projectUpdatePayload: ProjectUpdateRequest, requestingUser: UserADM ): Task[ProjectOperationResponseADM] = // check if the requesting user is allowed to perform updates if (!requestingUser.permissions.isProjectAdmin(projectIri.value) && !requestingUser.permissions.isSystemAdmin) { ZIO.fail(ForbiddenException("Project's information can only be changed by a project or system admin.")) } else { - for { - result <- updateProjectADM(projectIri = projectIri, projectUpdatePayload = projectUpdatePayload) - - } yield result + updateProjectADM(projectIri, projectUpdatePayload) } - val task = changeProjectTask(projectIri, updatePayload, user) + val task = changeProjectTask(projectIri, projectUpdate, user) IriLocker.runWithIriLock(apiRequestID, projectIri.value, task) } @@ -504,7 +503,7 @@ final case class ProjectsResponderADMLive( * * [[NotFoundException]] In the case that the project's IRI is not found. */ - private def updateProjectADM(projectIri: Iri.ProjectIri, projectUpdatePayload: ProjectUpdatePayloadADM) = { + private def updateProjectADM(projectIri: Iri.ProjectIri, projectUpdatePayload: ProjectUpdateRequest) = { val areAllParamsNone: Boolean = projectUpdatePayload.productIterator.forall { case param: Option[Any] => param.isEmpty @@ -513,7 +512,7 @@ final case class ProjectsResponderADMLive( if (areAllParamsNone) { ZIO.fail(BadRequestException("No data would be changed. Aborting update request.")) } else { - val projectId = IriIdentifier(projectIri) + val projectId = IriIdentifier.from(projectIri) for { _ <- projectService .findByProjectIdentifier(projectId) @@ -565,7 +564,7 @@ final case class ProjectsResponderADMLive( */ private def checkProjectUpdate( updatedProject: ProjectADM, - projectUpdatePayload: ProjectUpdatePayloadADM + projectUpdatePayload: ProjectUpdateRequest ): Task[Unit] = ZIO.attempt { if (projectUpdatePayload.shortname.nonEmpty) { projectUpdatePayload.shortname @@ -653,7 +652,7 @@ final case class ProjectsResponderADMLive( /** * Creates a project. * - * @param createPayload the new project's information. + * @param projectCreate the new project's information. * * @param requestingUser the user that is making the request. * @param apiRequestID the unique api request ID. @@ -666,7 +665,7 @@ final case class ProjectsResponderADMLive( * [[BadRequestException]] In the case when the shortcode is invalid. */ override def projectCreateRequestADM( - createPayload: ProjectCreatePayloadADM, + projectCreate: ProjectCreateRequest, requestingUser: UserADM, apiRequestID: UUID ): Task[ProjectOperationResponseADM] = { @@ -750,7 +749,7 @@ final case class ProjectsResponderADMLive( } yield () def projectCreateTask( - createProjectRequest: ProjectCreatePayloadADM, + createProjectRequest: ProjectCreateRequest, requestingUser: UserADM ): Task[ProjectOperationResponseADM] = for { @@ -820,7 +819,7 @@ final case class ProjectsResponderADMLive( } yield ProjectOperationResponseADM(project = newProjectADM.unescape) - val task = projectCreateTask(createPayload, requestingUser) + val task = projectCreateTask(projectCreate, requestingUser) IriLocker.runWithIriLock(apiRequestID, PROJECTS_GLOBAL_LOCK_IRI, task) } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala index c6d38e2ddb..0139c27db4 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala @@ -16,7 +16,7 @@ import org.apache.pekko.http.scaladsl.model.HttpMethods.POST import org.apache.pekko.http.scaladsl.model.HttpMethods.PUT import zio._ -import scala.concurrent.ExecutionContext +import scala.concurrent.ExecutionContextExecutor import org.knora.webapi.config.AppConfig import org.knora.webapi.core @@ -30,8 +30,10 @@ import org.knora.webapi.responders.v2.ValuesResponderV2 import org.knora.webapi.routing import org.knora.webapi.routing.admin._ import org.knora.webapi.routing.v2._ +import org.knora.webapi.slice.admin.api.ProjectsEndpointsHandler import org.knora.webapi.slice.admin.api.service.ProjectADMRestService import org.knora.webapi.slice.admin.domain.service.KnoraProjectRepo +import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter import org.knora.webapi.slice.ontology.api.service.RestCardinalityService import org.knora.webapi.slice.resourceinfo.api.RestResourceInfoService import org.knora.webapi.slice.resourceinfo.domain.IriConverter @@ -58,7 +60,7 @@ object ApiRoutes { with KnoraProjectRepo with MessageRelay with ProjectADMRestService - with ProjectsEndpointsHandlerF + with ProjectsEndpointsHandler with RestCardinalityService with RestResourceInfoService with StringFormatter @@ -72,7 +74,7 @@ object ApiRoutes { sys <- ZIO.service[ActorSystem] router <- ZIO.service[AppRouter] appConfig <- ZIO.service[AppConfig] - projectsHandler <- ZIO.service[ProjectsEndpointsHandlerF] + projectsHandler <- ZIO.service[ProjectsEndpointsHandler] routeData <- ZIO.succeed(KnoraRouteData(sys.system, router.ref, appConfig)) tapirToPekkoRoute = TapirToPekkoInterpreter()(sys.system.dispatcher) runtime <- ZIO.runtime[ @@ -101,7 +103,7 @@ object ApiRoutes { */ private final case class ApiRoutesImpl( routeData: KnoraRouteData, - projectsHandler: ProjectsEndpointsHandlerF, + projectsHandler: ProjectsEndpointsHandler, tapirToPekkoRoute: TapirToPekkoInterpreter, appConfig: AppConfig, implicit val runtime: Runtime[ @@ -120,8 +122,8 @@ private final case class ApiRoutesImpl( ) extends ApiRoutes with AroundDirectives { - private implicit val system: actor.ActorSystem = routeData.system - private implicit val executionContext: ExecutionContext = system.dispatcher + implicit val system: actor.ActorSystem = routeData.system + implicit val executionContext: ExecutionContextExecutor = routeData.system.dispatcher val routes: Route = logDuration { @@ -132,23 +134,24 @@ private final case class ApiRoutesImpl( .withAllowedMethods(List(GET, PUT, POST, DELETE, PATCH, HEAD, OPTIONS)) ) { DSPApiDirectives.handleErrors(appConfig) { - HealthRoute().makeRoute ~ - VersionRoute().makeRoute ~ - RejectingRoute(appConfig, runtime).makeRoute ~ - OntologiesRouteV2().makeRoute ~ - SearchRouteV2(appConfig.v2.fulltextSearch.searchValueMinLength).makeRoute ~ - ResourcesRouteV2(appConfig).makeRoute ~ - ValuesRouteV2().makeRoute ~ - StandoffRouteV2().makeRoute ~ - ListsRouteV2().makeRoute ~ + val adminProjectsRoutes = projectsHandler.allHanders.map(tapirToPekkoRoute.toRoute(_)).reduce(_ ~ _) + adminProjectsRoutes ~ AuthenticationRouteV2().makeRoute ~ + FilesRouteADM(routeData, runtime).makeRoute ~ GroupsRouteADM(routeData, runtime).makeRoute ~ + HealthRoute().makeRoute ~ ListsRouteADM(routeData, runtime).makeRoute ~ + ListsRouteV2().makeRoute ~ + OntologiesRouteV2().makeRoute ~ PermissionsRouteADM(routeData, runtime).makeRoute ~ - ProjectsRouteADM(tapirToPekkoRoute, projectsHandler).makeRoute ~ + RejectingRoute(appConfig, runtime).makeRoute ~ + ResourcesRouteV2(appConfig).makeRoute ~ + SearchRouteV2(appConfig.v2.fulltextSearch.searchValueMinLength).makeRoute ~ + StandoffRouteV2().makeRoute ~ StoreRouteADM(routeData, runtime).makeRoute ~ UsersRouteADM().makeRoute ~ - FilesRouteADM(routeData, runtime).makeRoute + ValuesRouteV2().makeRoute ~ + VersionRoute().makeRoute } } } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala b/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala index 0e6bd87849..00f85603e0 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala @@ -81,6 +81,9 @@ trait Authenticator { * [[AuthenticationException]] when the IRI can not be found inside the token, which is probably a bug. */ def getUserADMThroughCredentialsV2(credentials: Option[KnoraCredentialsV2]): Task[UserADM] + def verifyJwt(jwtToken: String): Task[UserADM] = getUserADMThroughCredentialsV2( + Some(KnoraJWTTokenCredentialsV2(jwtToken)) + ) /** * Used to logout the user, i.e. returns a header deleting the cookie and puts the token on the 'invalidated' list. diff --git a/webapi/src/main/scala/org/knora/webapi/routing/BaseEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/routing/BaseEndpoints.scala deleted file mode 100644 index f2e38056cb..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/routing/BaseEndpoints.scala +++ /dev/null @@ -1,35 +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 - -import sttp.model.StatusCode -import sttp.tapir.EndpointOutput -import sttp.tapir.endpoint -import sttp.tapir.generic.auto._ -import sttp.tapir.json.zio.jsonBody -import sttp.tapir.oneOf -import sttp.tapir.oneOfVariant -import sttp.tapir.statusCode -import zio.ZLayer - -import dsp.errors.BadRequestException -import dsp.errors.NotFoundException -import dsp.errors.RequestRejectedException - -final case class BaseEndpoints() { - - private val defaultErrorOutputs: EndpointOutput.OneOf[RequestRejectedException, RequestRejectedException] = - oneOf[RequestRejectedException]( - oneOfVariant[NotFoundException](statusCode(StatusCode.NotFound).and(jsonBody[NotFoundException])), - oneOfVariant[BadRequestException](statusCode(StatusCode.BadRequest).and(jsonBody[BadRequestException])) - ) - - val publicEndpoint = endpoint.errorOut(defaultErrorOutputs) -} - -object BaseEndpoints { - val layer = ZLayer.derive[BaseEndpoints] -} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/HandlerMapperF.scala b/webapi/src/main/scala/org/knora/webapi/routing/HandlerMapperF.scala deleted file mode 100644 index 579bf465c0..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/routing/HandlerMapperF.scala +++ /dev/null @@ -1,41 +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 - -import sttp.tapir.Endpoint -import sttp.tapir.server.ServerEndpoint.Full -import zio.Task -import zio.ZIO -import zio.ZLayer - -import scala.concurrent.Future - -import dsp.errors.RequestRejectedException - -case class EndpointAndZioHandler[INPUT, OUTPUT]( - endpoint: Endpoint[Unit, INPUT, RequestRejectedException, OUTPUT, Any], - handler: INPUT => Task[OUTPUT] -) - -final case class HandlerMapperF()(implicit val r: zio.Runtime[Any]) { - - def mapEndpointAndHandler[INPUT, OUTPUT]( - handler: EndpointAndZioHandler[INPUT, OUTPUT] - ): Full[Unit, Unit, INPUT, RequestRejectedException, OUTPUT, Any, Future] = - handler.endpoint.serverLogic[Future](handlerFromZio(handler.handler)) - - private def runToFuture[OUTPUT](zio: Task[OUTPUT]): Future[Either[RequestRejectedException, OUTPUT]] = - UnsafeZioRun.runToFuture(zio.refineOrDie { case e: RequestRejectedException => e }.either) - - private def handlerFromZio[INPUT, OUTPUT]( - zio: INPUT => Task[OUTPUT] - ): INPUT => Future[Either[RequestRejectedException, OUTPUT]] = - input => runToFuture(zio.apply(input)) -} - -object HandlerMapperF { - val layer = ZLayer.fromZIO(ZIO.runtime[Any].map(HandlerMapperF()(_))) -} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsEndpoints.scala deleted file mode 100644 index 9332a5166a..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsEndpoints.scala +++ /dev/null @@ -1,93 +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 sttp.tapir._ -import sttp.tapir.generic.auto._ -import sttp.tapir.json.spray.{jsonBody => sprayJsonBody} -import zio.ZLayer - -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetResponseADM -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectKeywordsGetResponseADM -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectRestrictedViewSettingsGetResponseADM -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectsADMJsonProtocol -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectsGetResponseADM -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectsKeywordsGetResponseADM -import org.knora.webapi.routing.BaseEndpoints -import org.knora.webapi.routing.PathVariables.projectIri -import org.knora.webapi.routing.PathVariables.projectShortcode -import org.knora.webapi.routing.PathVariables.projectShortname - -final case class ProjectsEndpoints( - baseEndpoints: BaseEndpoints -) extends ProjectsADMJsonProtocol { - - private val projectsBase = "admin" / "projects" - private val projectsByIri = projectsBase / "iri" / projectIri - private val projectsByShortcode = projectsBase / "shortcode" / projectShortcode - private val projectsByShortname = projectsBase / "shortname" / projectShortname - private val keywords = "Keywords" - private val restrictedViewSettings = "RestrictedViewSettings" - private val tags = List("Projects", "Admin API") - - val getAdminProjects = baseEndpoints.publicEndpoint.get - .in(projectsBase) - .out(sprayJsonBody[ProjectsGetResponseADM]) - .description("Returns all projects.") - .tags(tags) - - val getAdminProjectsKeywords = baseEndpoints.publicEndpoint.get - .in(projectsBase / keywords) - .out(sprayJsonBody[ProjectsKeywordsGetResponseADM]) - .description("Returns all unique keywords for all projects as a list.") - .tags(tags) - - val getAdminProjectsByProjectIri = baseEndpoints.publicEndpoint.get - .in(projectsByIri) - .out(sprayJsonBody[ProjectGetResponseADM]) - .description("Returns a single project identified through the IRI.") - .tags(tags) - - val getAdminProjectsByProjectShortcode = baseEndpoints.publicEndpoint.get - .in(projectsByShortcode) - .out(sprayJsonBody[ProjectGetResponseADM]) - .description("Returns a single project identified through the shortcode.") - .tags(tags) - - val getAdminProjectsByProjectShortname = baseEndpoints.publicEndpoint.get - .in(projectsByShortname) - .out(sprayJsonBody[ProjectGetResponseADM]) - .description("Returns a single project identified through the shortname.") - .tags(tags) - - val getAdminProjectsKeywordsByProjectIri = baseEndpoints.publicEndpoint.get - .in(projectsByIri / keywords) - .out(sprayJsonBody[ProjectKeywordsGetResponseADM]) - .description("Returns all keywords for a single project.") - .tags(tags) - - val getAdminProjectsByProjectIriRestrictedViewSettings = baseEndpoints.publicEndpoint.get - .in(projectsByIri / restrictedViewSettings) - .out(sprayJsonBody[ProjectRestrictedViewSettingsGetResponseADM]) - .description("Returns the project's restricted view settings identified through the IRI.") - .tags(tags) - - val getAdminProjectsByProjectShortcodeRestrictedViewSettings = baseEndpoints.publicEndpoint.get - .in(projectsByShortcode / restrictedViewSettings) - .out(sprayJsonBody[ProjectRestrictedViewSettingsGetResponseADM]) - .description("Returns the project's restricted view settings identified through the shortcode.") - .tags(tags) - - val getAdminProjectsByProjectShortnameRestrictedViewSettings = baseEndpoints.publicEndpoint.get - .in(projectsByShortname / restrictedViewSettings) - .out(sprayJsonBody[ProjectRestrictedViewSettingsGetResponseADM]) - .description("Returns the project's restricted view settings identified through the shortname.") - .tags(tags) -} - -object ProjectsEndpoints { - val layer = ZLayer.derive[ProjectsEndpoints] -} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsEndpointsHandlerF.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsEndpointsHandlerF.scala deleted file mode 100644 index 3bd0a5a155..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsEndpointsHandlerF.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.ZLayer - -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.IriIdentifier -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.ShortcodeIdentifier -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.ShortnameIdentifier -import org.knora.webapi.routing.EndpointAndZioHandler -import org.knora.webapi.routing.HandlerMapperF -import org.knora.webapi.slice.admin.api.service.ProjectADMRestService - -final case class ProjectsEndpointsHandlerF( - projectsEndpoints: ProjectsEndpoints, - restService: ProjectADMRestService, - mapper: HandlerMapperF -) { - - val getAdminProjectsHandler = - EndpointAndZioHandler(projectsEndpoints.getAdminProjects, (_: Unit) => restService.getProjectsADMRequest()) - - val getAdminProjectsKeywordsHandler = - EndpointAndZioHandler( - projectsEndpoints.getAdminProjectsKeywords, - (_: Unit) => restService.getKeywords() - ) - - val getAdminProjectsByProjectIriHandler = - EndpointAndZioHandler( - projectsEndpoints.getAdminProjectsByProjectIri, - (id: IriIdentifier) => restService.getSingleProjectADMRequest(id) - ) - - val getAdminProjectsByProjectShortcodeHandler = - EndpointAndZioHandler( - projectsEndpoints.getAdminProjectsByProjectShortcode, - (id: ShortcodeIdentifier) => restService.getSingleProjectADMRequest(id) - ) - - val getAdminProjectsByProjectShortnameHandler = - EndpointAndZioHandler( - projectsEndpoints.getAdminProjectsByProjectShortname, - (id: ShortnameIdentifier) => restService.getSingleProjectADMRequest(id) - ) - - val getAdminProjectsKeywordsByProjectIriHandler = - EndpointAndZioHandler( - projectsEndpoints.getAdminProjectsKeywordsByProjectIri, - (iri: IriIdentifier) => restService.getKeywordsByProjectIri(iri.value) - ) - - val getAdminProjectByProjectIriRestrictedViewSettingsHandler = - EndpointAndZioHandler( - projectsEndpoints.getAdminProjectsByProjectIriRestrictedViewSettings, - (id: IriIdentifier) => restService.getProjectRestrictedViewSettings(id) - ) - - val getAdminProjectByProjectShortcodeRestrictedViewSettingsHandler = - EndpointAndZioHandler( - projectsEndpoints.getAdminProjectsByProjectShortcodeRestrictedViewSettings, - (id: ShortcodeIdentifier) => restService.getProjectRestrictedViewSettings(id) - ) - - val getAdminProjectByProjectShortnameRestrictedViewSettingsHandler = - EndpointAndZioHandler( - projectsEndpoints.getAdminProjectsByProjectShortnameRestrictedViewSettings, - (id: ShortnameIdentifier) => restService.getProjectRestrictedViewSettings(id) - ) - - val handlers = - List( - getAdminProjectsHandler, - getAdminProjectsKeywordsHandler, - getAdminProjectsByProjectIriHandler, - getAdminProjectsByProjectShortcodeHandler, - getAdminProjectsByProjectShortnameHandler, - getAdminProjectsKeywordsByProjectIriHandler, - getAdminProjectByProjectIriRestrictedViewSettingsHandler, - getAdminProjectByProjectShortcodeRestrictedViewSettingsHandler, - getAdminProjectByProjectShortnameRestrictedViewSettingsHandler - ).map(mapper.mapEndpointAndHandler(_)) -} - -object ProjectsEndpointsHandlerF { - val layer = ZLayer.derive[ProjectsEndpointsHandlerF] -} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala deleted file mode 100644 index 0f9bb48c05..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteADM.scala +++ /dev/null @@ -1,230 +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 org.apache.pekko.Done -import org.apache.pekko.http.scaladsl.model.ContentTypes -import org.apache.pekko.http.scaladsl.model.HttpEntity -import org.apache.pekko.http.scaladsl.model.headers.ContentDispositionTypes -import org.apache.pekko.http.scaladsl.model.headers.`Content-Disposition` -import org.apache.pekko.http.scaladsl.server.Directives._ -import org.apache.pekko.http.scaladsl.server.PathMatcher -import org.apache.pekko.http.scaladsl.server.RequestContext -import org.apache.pekko.http.scaladsl.server.Route -import org.apache.pekko.stream.IOResult -import org.apache.pekko.stream.scaladsl.FileIO -import zio._ - -import java.nio.file.Files -import scala.concurrent.ExecutionContext -import scala.concurrent.Future -import scala.util.Try - -import dsp.errors.BadRequestException -import dsp.valueobjects.Iri -import dsp.valueobjects.Iri.ProjectIri -import dsp.valueobjects.Project._ -import org.knora.webapi.IRI -import org.knora.webapi.core.MessageRelay -import org.knora.webapi.messages.StringFormatter -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM._ -import org.knora.webapi.messages.admin.responder.projectsmessages._ -import org.knora.webapi.routing.Authenticator -import org.knora.webapi.routing.RouteUtilADM._ -import org.knora.webapi.routing._ -import org.knora.webapi.slice.admin.api.service.ProjectADMRestService - -final case class ProjectsRouteADM( - interpreter: TapirToPekkoInterpreter, - projectsEndpointsHandlerF: ProjectsEndpointsHandlerF -)( - private implicit val runtime: Runtime[ - org.knora.webapi.routing.Authenticator with StringFormatter with MessageRelay with ProjectADMRestService - ], - private implicit val executionContext: ExecutionContext -) extends ProjectsADMJsonProtocol { - - private val tapirRoutes: Route = projectsEndpointsHandlerF.handlers.map(interpreter.toRoute(_)).reduce(_ ~ _) - - private val projectsBasePath: PathMatcher[Unit] = PathMatcher("admin" / "projects") - - def makeRoute: Route = - tapirRoutes ~ - addProject() ~ - changeProject() ~ - deleteProject() ~ - getProjectMembersByIri() ~ - getProjectMembersByShortname() ~ - getProjectMembersByShortcode() ~ - getProjectAdminMembersByIri() ~ - getProjectAdminMembersByShortname() ~ - getProjectAdminMembersByShortcode() ~ - getProjectData() ~ - postExportProject - - /** - * Creates a new project. - */ - private def addProject(): Route = path(projectsBasePath) { - post { - entity(as[CreateProjectApiRequestADM]) { apiRequest => requestContext => - val requestTask = for { - projectCreatePayload <- ProjectCreatePayloadADM.make(apiRequest).toZIO - requestingUser <- Authenticator.getUserADM(requestContext) - uuid <- RouteUtilZ.randomUuid() - } yield ProjectCreateRequestADM(projectCreatePayload, requestingUser, uuid) - runJsonRouteZ(requestTask, requestContext) - } - } - } - - /** - * Updates a project identified by the IRI. - */ - private def changeProject(): Route = - path(projectsBasePath / "iri" / Segment) { value => - put { - entity(as[ChangeProjectApiRequestADM]) { apiRequest => requestContext => - val getProjectIri = - ZIO - .fromOption(Iri.validateAndEscapeProjectIri(value)) - .orElseFail(BadRequestException(s"Invalid project IRI $value")) - .flatMap(ProjectIri.make(_).toZIO) - - val requestTask = for { - projectIri <- getProjectIri - projectUpdatePayload <- ProjectUpdatePayloadADM.make(apiRequest).toZIO - requestingUser <- Authenticator.getUserADM(requestContext) - uuid <- RouteUtilZ.randomUuid() - } yield ProjectChangeRequestADM(projectIri, projectUpdatePayload, requestingUser, uuid) - runJsonRouteZ(requestTask, requestContext) - } - } - } - - /** - * Updates project status to false. - */ - private def deleteProject(): Route = - path(projectsBasePath / "iri" / Segment) { value => - delete { requestContext => - val requestTask = for { - iri <- ProjectIri.make(value).toZIO.orElseFail(BadRequestException(s"Invalid Project IRI $value")) - status <- ProjectStatus.make(false).toZIO.orElseFail(BadRequestException(s"Invalid Project Status")) - payload = ProjectUpdatePayloadADM(status = Some(status)) - user <- Authenticator.getUserADM(requestContext) - uuid <- RouteUtilZ.randomUuid() - } yield ProjectChangeRequestADM(iri, payload, user, uuid) - runJsonRouteZ(requestTask, requestContext) - } - } - - /** - * Returns all members of a project identified through the IRI. - */ - private def getProjectMembersByIri(): Route = - path(projectsBasePath / "iri" / Segment / "members") { value => - get(getProjectMembers(IriIdentifier.fromString(value).toZIO, _)) - } - - private def getProjectMembers(idTask: Task[ProjectIdentifierADM], requestContext: RequestContext) = { - val requestTask = for { - id <- idTask.mapError(e => BadRequestException(e.getMessage)) - requestingUser <- Authenticator.getUserADM(requestContext) - } yield ProjectMembersGetRequestADM(id, requestingUser) - runJsonRouteZ(requestTask, requestContext) - } - - /** - * Returns all members of a project identified through the shortname. - */ - private def getProjectMembersByShortname(): Route = - path(projectsBasePath / "shortname" / Segment / "members") { value => - get(getProjectMembers(ShortnameIdentifier.fromString(value).toZIO, _)) - } - - /** - * Returns all members of a project identified through the shortcode. - */ - private def getProjectMembersByShortcode(): Route = - path(projectsBasePath / "shortcode" / Segment / "members") { value => - get(getProjectMembers(ShortcodeIdentifier.fromString(value).toZIO, _)) - } - - /** - * Returns all admin members of a project identified through the IRI. - */ - private def getProjectAdminMembersByIri(): Route = - path(projectsBasePath / "iri" / Segment / "admin-members") { value => - get(getProjectAdminMembers(IriIdentifier.fromString(value).toZIO, _)) - } - - private def getProjectAdminMembers(idTask: Task[ProjectIdentifierADM], requestContext: RequestContext) = { - val requestTask = for { - id <- idTask.mapError(e => BadRequestException(e.getMessage)) - requestingUser <- Authenticator.getUserADM(requestContext) - } yield ProjectAdminMembersGetRequestADM(id, requestingUser) - runJsonRouteZ(requestTask, requestContext) - } - - /** - * Returns all admin members of a project identified through the shortname. - */ - private def getProjectAdminMembersByShortname(): Route = - path(projectsBasePath / "shortname" / Segment / "admin-members") { value => - get(getProjectAdminMembers(ShortnameIdentifier.fromString(value).toZIO, _)) - } - - /** - * Returns all admin members of a project identified through shortcode. - */ - private def getProjectAdminMembersByShortcode(): Route = - path(projectsBasePath / "shortcode" / Segment / "admin-members") { value => - get(getProjectAdminMembers(ShortcodeIdentifier.fromString(value).toZIO, _)) - } - - private val projectDataHeader = - `Content-Disposition`(ContentDispositionTypes.attachment, Map(("filename", "project-data.trig"))) - - /** - * Returns all ontologies, data, and configuration belonging to a project. - */ - private def getProjectData(): Route = - path(projectsBasePath / "iri" / Segment / "AllData") { projectIri: IRI => - get(respondWithHeaders(projectDataHeader)(getProjectDataEntity(projectIri))) - } - - private def getProjectDataEntity(projectIri: IRI): Route = { requestContext => - UnsafeZioRun.runToFuture { - val requestTask = for { - id <- IriIdentifier.fromString(projectIri).toZIO - requestingUser <- Authenticator.getUserADM(requestContext) - projectData <- ProjectADMRestService.getAllProjectData(id, requestingUser) - response <- ZIO.attemptBlocking( - HttpEntity( - ContentTypes.`application/octet-stream`, - FileIO.fromPath(projectData.projectDataFile).watchTermination() { - case (_: Future[IOResult], result: Future[Done]) => - result.onComplete((_: Try[Done]) => Files.delete(projectData.projectDataFile)) - } - ) - ) - } yield response - requestTask.flatMap(response => ZIO.fromFuture(_ => requestContext.complete(response))) - } - } - - private def postExportProject: Route = - path(projectsBasePath / "iri" / Segment / "export") { projectIri => - post { ctx => - val requestTask = for { - requestingUser <- Authenticator.getUserADM(ctx) - _ <- ProjectADMRestService.exportProject(projectIri, requestingUser) - } yield RouteUtilADM.acceptedResponse("work in progress") - RouteUtilADM.completeContext(ctx, requestTask) - } - } -} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala index eca18d3243..ebfb90565f 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala @@ -15,17 +15,16 @@ import java.nio.file.Files import dsp.errors.BadRequestException import dsp.valueobjects.Iri._ -import dsp.valueobjects.RestrictedViewSize 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.ProjectCreatePayloadADM 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.projectsmessages.ProjectSetRestrictedViewSizePayload -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectUpdatePayloadADM 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( @@ -102,48 +101,48 @@ final case class ProjectsRouteZ( private def getProjects(): Task[Response] = for { - r <- projectsService.getProjectsADMRequest() + 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.getSingleProjectADMRequest(iriIdentifier) + 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.getSingleProjectADMRequest(shortnameIdentifier) + 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.getSingleProjectADMRequest(shortcodeIdentifier) + 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[ProjectCreatePayloadADM]).mapError(e => BadRequestException(e)) - r <- projectsService.createProjectADMRequest(payload, requestingUser) + 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.") - projectIri <- ProjectIri.make(iriDecoded).toZIO.mapError(e => BadRequestException(e.msg)) - r <- projectsService.deleteProject(projectIri, requestingUser) - } yield Response.json(r.toJsValue.toString()) + 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.mapError(e => BadRequestException(e.msg)) + projectIri <- ProjectIri.make(iriDecoded).toZIO.mapBoth(e => BadRequestException(e.msg), IriIdentifier.from) body <- request.body.asString - payload <- ZIO.fromEither(body.fromJson[ProjectUpdatePayloadADM]).mapError(e => BadRequestException(e)) + payload <- ZIO.fromEither(body.fromJson[ProjectUpdateRequest]).mapError(e => BadRequestException(e)) r <- projectsService.updateProject(projectIri, payload, requestingUser) } yield Response.json(r.toJsValue.toString) @@ -182,43 +181,43 @@ final case class ProjectsRouteZ( 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(iriIdentifier, requestingUser) + 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(shortnameIdentifier, requestingUser) + 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(shortcodeIdentifier, requestingUser) + 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.getProjectAdmins(iriIdentifier, requestingUser) + 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.getProjectAdmins(shortnameIdentifier, requestingUser) + 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.getProjectAdmins(shortcodeIdentifier, requestingUser) + r <- projectsService.getProjectAdminMembers(requestingUser, shortcodeIdentifier) } yield Response.json(r.toJsValue.toString()) private def getKeywords(): Task[Response] = for { - r <- projectsService.getKeywords() + r <- projectsService.listAllKeywords() } yield Response.json(r.toJsValue.toString) private def getKeywordsByProjectIri(iriUrlEncoded: String): Task[Response] = @@ -263,9 +262,8 @@ final case class ProjectsRouteZ( private def handleRestrictedViewSizeRequest(id: ProjectIdentifierADM, body: Body, user: UserADM) = for { body <- body.asString - payload <- ZIO.fromEither(body.fromJson[ProjectSetRestrictedViewSizePayload]).mapError(BadRequestException(_)) - size <- ZIO.fromEither(RestrictedViewSize.make(payload.size)).mapError(BadRequestException(_)) - response <- projectsService.setProjectRestrictedViewSettings(id, user, size) + payload <- ZIO.fromEither(body.fromJson[ProjectSetRestrictedViewSizeRequest]).mapError(BadRequestException(_)) + response <- projectsService.updateProjectRestrictedViewSettings(id, user, payload) } yield Response.json(response.toJson) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpoints.scala new file mode 100644 index 0000000000..715c44437f --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpoints.scala @@ -0,0 +1,202 @@ +/* + * 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.slice.admin.api + +import sttp.capabilities.pekko.PekkoStreams +import sttp.model.StatusCode +import sttp.tapir._ +import sttp.tapir.generic.auto._ +import sttp.tapir.json.spray.{jsonBody => sprayJsonBody} +import sttp.tapir.json.zio.{jsonBody => zioJsonBody} +import zio.Chunk +import zio.ZLayer + +import org.knora.webapi.messages.admin.responder.projectsmessages._ +import org.knora.webapi.routing.PathVariables.projectIri +import org.knora.webapi.routing.PathVariables.projectShortcode +import org.knora.webapi.routing.PathVariables.projectShortname +import org.knora.webapi.slice.admin.api.model.ProjectExportInfoResponse +import org.knora.webapi.slice.admin.api.model.ProjectImportResponse +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.common.api.BaseEndpoints + +final case class ProjectsEndpoints( + baseEndpoints: BaseEndpoints +) extends ProjectsADMJsonProtocol { + + private val projectsBase = "admin" / "projects" + private val projectsByIri = projectsBase / "iri" / projectIri + private val projectsByShortcode = projectsBase / "shortcode" / projectShortcode + private val projectsByShortname = projectsBase / "shortname" / projectShortname + + // other path elements + private val keywords = "Keywords" + private val `export` = "export" + private val members = "members" + private val adminMembers = "admin-members" + private val restrictedViewSettings = "RestrictedViewSettings" + + private val tags = List("Projects", "Admin API") + + object Public { + + val getAdminProjects = baseEndpoints.publicEndpoint.get + .in(projectsBase) + .out(sprayJsonBody[ProjectsGetResponseADM]) + .description("Returns all projects.") + .tags(tags) + + val getAdminProjectsKeywords = baseEndpoints.publicEndpoint.get + .in(projectsBase / keywords) + .out(sprayJsonBody[ProjectsKeywordsGetResponseADM]) + .description("Returns all unique keywords for all projects as a list.") + .tags(tags) + + val getAdminProjectsByProjectIri = baseEndpoints.publicEndpoint.get + .in(projectsByIri) + .out(sprayJsonBody[ProjectGetResponseADM]) + .description("Returns a single project identified by the IRI.") + .tags(tags) + + val getAdminProjectsByProjectShortcode = baseEndpoints.publicEndpoint.get + .in(projectsByShortcode) + .out(sprayJsonBody[ProjectGetResponseADM]) + .description("Returns a single project identified by the shortcode.") + .tags(tags) + + val getAdminProjectsByProjectShortname = baseEndpoints.publicEndpoint.get + .in(projectsByShortname) + .out(sprayJsonBody[ProjectGetResponseADM]) + .description("Returns a single project identified by the shortname.") + .tags(tags) + + val getAdminProjectsKeywordsByProjectIri = baseEndpoints.publicEndpoint.get + .in(projectsByIri / keywords) + .out(sprayJsonBody[ProjectKeywordsGetResponseADM]) + .description("Returns all keywords for a single project.") + .tags(tags) + + val getAdminProjectsByProjectIriRestrictedViewSettings = baseEndpoints.publicEndpoint.get + .in(projectsByIri / restrictedViewSettings) + .out(sprayJsonBody[ProjectRestrictedViewSettingsGetResponseADM]) + .description("Returns the project's restricted view settings identified by the IRI.") + .tags(tags) + + val getAdminProjectsByProjectShortcodeRestrictedViewSettings = baseEndpoints.publicEndpoint.get + .in(projectsByShortcode / restrictedViewSettings) + .out(sprayJsonBody[ProjectRestrictedViewSettingsGetResponseADM]) + .description("Returns the project's restricted view settings identified by the shortcode.") + .tags(tags) + + val getAdminProjectsByProjectShortnameRestrictedViewSettings = baseEndpoints.publicEndpoint.get + .in(projectsByShortname / restrictedViewSettings) + .out(sprayJsonBody[ProjectRestrictedViewSettingsGetResponseADM]) + .description("Returns the project's restricted view settings identified by the shortname.") + .tags(tags) + } + + object Secured { + val setAdminProjectsByProjectIriRestrictedViewSettings = baseEndpoints.securedEndpoint.post + .in(projectsByIri / restrictedViewSettings) + .in(zioJsonBody[ProjectSetRestrictedViewSizeRequest]) + .out(zioJsonBody[ProjectRestrictedViewSizeResponseADM]) + .description("Sets the project's restricted view settings identified by the IRI.") + .tags(tags) + + val setAdminProjectsByProjectShortcodeRestrictedViewSettings = baseEndpoints.securedEndpoint.post + .in(projectsByShortcode / restrictedViewSettings) + .in(zioJsonBody[ProjectSetRestrictedViewSizeRequest]) + .out(zioJsonBody[ProjectRestrictedViewSizeResponseADM]) + .description("Sets the project's restricted view settings identified by the shortcode.") + .tags(tags) + + val getAdminProjectsByProjectIriMembers = baseEndpoints.securedEndpoint.get + .in(projectsByIri / members) + .out(sprayJsonBody[ProjectMembersGetResponseADM]) + .description("Returns all project members of a project identified by the IRI.") + .tags(tags) + + val getAdminProjectsByProjectShortcodeMembers = baseEndpoints.securedEndpoint.get + .in(projectsByShortcode / members) + .out(sprayJsonBody[ProjectMembersGetResponseADM]) + .description("Returns all project members of a project identified by the shortcode.") + .tags(tags) + + val getAdminProjectsByProjectShortnameMembers = baseEndpoints.securedEndpoint.get + .in(projectsByShortname / members) + .out(sprayJsonBody[ProjectMembersGetResponseADM]) + .description("Returns all project members of a project identified by the shortname.") + .tags(tags) + + val getAdminProjectsByProjectIriAdminMembers = baseEndpoints.securedEndpoint.get + .in(projectsByIri / adminMembers) + .out(sprayJsonBody[ProjectAdminMembersGetResponseADM]) + + val getAdminProjectsByProjectShortcodeAdminMembers = baseEndpoints.securedEndpoint.get + .in(projectsByShortcode / adminMembers) + .out(sprayJsonBody[ProjectAdminMembersGetResponseADM]) + .description("Returns all admin members of a project identified by the shortcode.") + .tags(tags) + + val getAdminProjectsByProjectShortnameAdminMembers = baseEndpoints.securedEndpoint.get + .in(projectsByShortname / adminMembers) + .out(sprayJsonBody[ProjectAdminMembersGetResponseADM]) + .description("Returns all admin members of a project identified by the shortname.") + .tags(tags) + + val deleteAdminProjectsByIri = baseEndpoints.securedEndpoint.delete + .in(projectsByIri) + .out(sprayJsonBody[ProjectOperationResponseADM]) + .description("Deletes a project identified by the IRI.") + .tags(tags) + + val getAdminProjectsExports = baseEndpoints.securedEndpoint.get + .in(projectsBase / `export`) + .out(zioJsonBody[Chunk[ProjectExportInfoResponse]]) + .description("Lists existing exports of all projects.") + .tags(tags) + + val postAdminProjectsByShortcodeExport = baseEndpoints.securedEndpoint.post + .in(projectsByShortcode / `export`) + .out(statusCode(StatusCode.Accepted)) + .description("Trigger an export of a project identified by the shortcode.") + .tags(tags) + + val postAdminProjectsByShortcodeImport = baseEndpoints.securedEndpoint.post + .in(projectsByShortcode / "import") + .out(zioJsonBody[ProjectImportResponse]) + .description("Trigger an import of a project identified by the shortcode.") + .tags(tags) + + val postAdminProjects = baseEndpoints.securedEndpoint.post + .in(projectsBase) + .in(zioJsonBody[ProjectCreateRequest]) + .out(sprayJsonBody[ProjectOperationResponseADM]) + .description("Creates a new project.") + .tags(tags) + + val putAdminProjectsByIri = baseEndpoints.securedEndpoint.put + .in(projectsByIri) + .in(zioJsonBody[ProjectUpdateRequest]) + .out(sprayJsonBody[ProjectOperationResponseADM]) + .description("Updates a project identified by the IRI.") + .tags(tags) + + val getAdminProjectsByIriAllData = baseEndpoints.securedEndpoint.get + .in(projectsByIri / "AllData") + .out(header[String]("Content-Disposition")) + .out(header[String]("Content-Type")) + .out(streamBinaryBody(PekkoStreams)(CodecFormat.OctetStream())) + .description("Returns all ontologies, data, and configuration belonging to a project identified by the IRI.") + .tags(tags) + } +} + +object ProjectsEndpoints { + val layer = ZLayer.derive[ProjectsEndpoints] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpointsHandler.scala new file mode 100644 index 0000000000..a895cb992e --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpointsHandler.scala @@ -0,0 +1,237 @@ +/* + * 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.slice.admin.api + +import org.apache.pekko.stream.scaladsl.FileIO +import zio.ZLayer + +import java.nio.file.Files +import scala.concurrent.ExecutionContext + +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.IriIdentifier +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.ShortcodeIdentifier +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.ShortnameIdentifier +import org.knora.webapi.messages.admin.responder.projectsmessages._ +import org.knora.webapi.messages.admin.responder.usersmessages.UserADM +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 +import org.knora.webapi.slice.common.api.EndpointAndZioHandler +import org.knora.webapi.slice.common.api.HandlerMapperF +import org.knora.webapi.slice.common.api.SecuredEndpointAndZioHandler + +final case class ProjectsEndpointsHandler( + projectsEndpoints: ProjectsEndpoints, + restService: ProjectADMRestService, + mapper: HandlerMapperF +) { + + val getAdminProjectsHandler = + EndpointAndZioHandler(projectsEndpoints.Public.getAdminProjects, (_: Unit) => restService.listAllProjects()) + + val getAdminProjectsKeywordsHandler = + EndpointAndZioHandler( + projectsEndpoints.Public.getAdminProjectsKeywords, + (_: Unit) => restService.listAllKeywords() + ) + + val getAdminProjectsByProjectIriHandler = + EndpointAndZioHandler( + projectsEndpoints.Public.getAdminProjectsByProjectIri, + (id: IriIdentifier) => restService.findProject(id) + ) + + val getAdminProjectsByProjectShortcodeHandler = + EndpointAndZioHandler( + projectsEndpoints.Public.getAdminProjectsByProjectShortcode, + (id: ShortcodeIdentifier) => restService.findProject(id) + ) + + val getAdminProjectsByProjectShortnameHandler = + EndpointAndZioHandler( + projectsEndpoints.Public.getAdminProjectsByProjectShortname, + (id: ShortnameIdentifier) => restService.findProject(id) + ) + + val getAdminProjectsKeywordsByProjectIriHandler = + EndpointAndZioHandler( + projectsEndpoints.Public.getAdminProjectsKeywordsByProjectIri, + (iri: IriIdentifier) => restService.getKeywordsByProjectIri(iri.value) + ) + + val getAdminProjectByProjectIriRestrictedViewSettingsHandler = + EndpointAndZioHandler( + projectsEndpoints.Public.getAdminProjectsByProjectIriRestrictedViewSettings, + (id: IriIdentifier) => restService.getProjectRestrictedViewSettings(id) + ) + + val getAdminProjectByProjectShortcodeRestrictedViewSettingsHandler = + EndpointAndZioHandler( + projectsEndpoints.Public.getAdminProjectsByProjectShortcodeRestrictedViewSettings, + (id: ShortcodeIdentifier) => restService.getProjectRestrictedViewSettings(id) + ) + + val getAdminProjectByProjectShortnameRestrictedViewSettingsHandler = + EndpointAndZioHandler( + projectsEndpoints.Public.getAdminProjectsByProjectShortnameRestrictedViewSettings, + (id: ShortnameIdentifier) => restService.getProjectRestrictedViewSettings(id) + ) + + // secured endpoints + val setAdminProjectsByProjectIriRestrictedViewSettingsHandler = + SecuredEndpointAndZioHandler[ + (IriIdentifier, ProjectSetRestrictedViewSizeRequest), + ProjectRestrictedViewSizeResponseADM + ]( + projectsEndpoints.Secured.setAdminProjectsByProjectIriRestrictedViewSettings, + user => { case (id, payload) => restService.updateProjectRestrictedViewSettings(id, user, payload) } + ) + + val setAdminProjectsByProjectShortcodeRestrictedViewSettingsHandler = + SecuredEndpointAndZioHandler[ + (ShortcodeIdentifier, ProjectSetRestrictedViewSizeRequest), + ProjectRestrictedViewSizeResponseADM + ]( + projectsEndpoints.Secured.setAdminProjectsByProjectShortcodeRestrictedViewSettings, + user => { case (id, payload) => + restService.updateProjectRestrictedViewSettings(id, user, payload) + } + ) + + val getAdminProjectsByProjectIriMembersHandler = + SecuredEndpointAndZioHandler( + projectsEndpoints.Secured.getAdminProjectsByProjectIriMembers, + user => id => restService.getProjectMembers(user, id) + ) + + val getAdminProjectsByProjectShortcodeMembersHandler = + SecuredEndpointAndZioHandler( + projectsEndpoints.Secured.getAdminProjectsByProjectShortcodeMembers, + user => id => restService.getProjectMembers(user, id) + ) + + val getAdminProjectsByProjectShortnameMembersHandler = + SecuredEndpointAndZioHandler( + projectsEndpoints.Secured.getAdminProjectsByProjectShortnameMembers, + user => id => restService.getProjectMembers(user, id) + ) + + val getAdminProjectsByProjectIriAdminMembersHandler = + SecuredEndpointAndZioHandler( + projectsEndpoints.Secured.getAdminProjectsByProjectIriAdminMembers, + user => id => restService.getProjectAdminMembers(user, id) + ) + + val getAdminProjectsByProjectShortcodeAdminMembersHandler = + SecuredEndpointAndZioHandler( + projectsEndpoints.Secured.getAdminProjectsByProjectShortcodeAdminMembers, + user => id => restService.getProjectAdminMembers(user, id) + ) + + val getAdminProjectsByProjectShortnameAdminMembersHandler = + SecuredEndpointAndZioHandler( + projectsEndpoints.Secured.getAdminProjectsByProjectShortnameAdminMembers, + user => id => restService.getProjectAdminMembers(user, id) + ) + + val deleteAdminProjectsByIriHandler = + SecuredEndpointAndZioHandler( + projectsEndpoints.Secured.deleteAdminProjectsByIri, + user => (id: IriIdentifier) => restService.deleteProject(id, user) + ) + + val getAdminProjectsExportsHandler = + SecuredEndpointAndZioHandler( + projectsEndpoints.Secured.getAdminProjectsExports, + user => (_: Unit) => restService.listExports(user) + ) + + val postAdminProjectsByShortcodeExportHandler = + SecuredEndpointAndZioHandler( + projectsEndpoints.Secured.postAdminProjectsByShortcodeExport, + user => (id: ShortcodeIdentifier) => restService.exportProject(id, user) + ) + + val postAdminProjectsByShortcodeImportHandler = + SecuredEndpointAndZioHandler( + projectsEndpoints.Secured.postAdminProjectsByShortcodeImport, + user => (id: ShortcodeIdentifier) => restService.importProject(id, user) + ) + + val postAdminProjectsHandler = + SecuredEndpointAndZioHandler( + projectsEndpoints.Secured.postAdminProjects, + user => (createReq: ProjectCreateRequest) => restService.createProject(createReq, user) + ) + + val putAdminProjectsByIriHandler = + SecuredEndpointAndZioHandler[(IriIdentifier, ProjectUpdateRequest), ProjectOperationResponseADM]( + projectsEndpoints.Secured.putAdminProjectsByIri, + user => { case (id: IriIdentifier, changeReq: ProjectUpdateRequest) => + restService.updateProject(id, changeReq, user) + } + ) + + val getAdminProjectsByIriAllDataHandler = { + implicit val ec: ExecutionContext = ExecutionContext.global + projectsEndpoints.Secured.getAdminProjectsByIriAllData.serverLogic((user: UserADM) => + (iri: IriIdentifier) => + // Future[Either[RequestRejectedException, (String, String, PekkoStreams.BinaryStream]] + mapper.runToFuture( + restService + .getAllProjectData(iri, user) + .map { result => + val path = result.projectDataFile +// On Pekko use pekko-streams to stream the file, but when running on zio-http we use ZStream: +// val stream = ZStream +// .fromPath(path) +// .ensuringWith(_ => ZIO.attempt(Files.deleteIfExists(path)).ignore) + val stream = FileIO + .fromPath(path) + .watchTermination() { case (_, result) => result.onComplete(_ => Files.deleteIfExists(path)) } + (s"attachment; filename=project-data.trig", "application/octet-stream", stream) + } + ) + ) + } + + val handlers = + List( + getAdminProjectsHandler, + getAdminProjectsKeywordsHandler, + getAdminProjectsByProjectIriHandler, + getAdminProjectsByProjectShortcodeHandler, + getAdminProjectsByProjectShortnameHandler, + getAdminProjectsKeywordsByProjectIriHandler, + getAdminProjectByProjectIriRestrictedViewSettingsHandler, + getAdminProjectByProjectShortcodeRestrictedViewSettingsHandler, + getAdminProjectByProjectShortnameRestrictedViewSettingsHandler + ).map(mapper.mapEndpointAndHandler(_)) + + val secureHandlers = getAdminProjectsByIriAllDataHandler :: List( + setAdminProjectsByProjectIriRestrictedViewSettingsHandler, + setAdminProjectsByProjectShortcodeRestrictedViewSettingsHandler, + getAdminProjectsByProjectIriMembersHandler, + getAdminProjectsByProjectShortcodeMembersHandler, + getAdminProjectsByProjectShortnameMembersHandler, + getAdminProjectsByProjectIriAdminMembersHandler, + getAdminProjectsByProjectShortcodeAdminMembersHandler, + getAdminProjectsByProjectShortnameAdminMembersHandler, + deleteAdminProjectsByIriHandler, + getAdminProjectsExportsHandler, + postAdminProjectsByShortcodeExportHandler, + postAdminProjectsByShortcodeImportHandler, + postAdminProjectsHandler, + putAdminProjectsByIriHandler + ).map(mapper.mapEndpointAndHandler(_)) + + val allHanders = handlers ++ secureHandlers +} + +object ProjectsEndpointsHandler { + val layer = ZLayer.derive[ProjectsEndpointsHandler] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectExportResponse.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectExportResponse.scala index 121c11908a..812ce68cd8 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectExportResponse.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectExportResponse.scala @@ -5,14 +5,14 @@ package org.knora.webapi.slice.admin.api.model -import zio.json.DeriveJsonEncoder -import zio.json.JsonEncoder +import zio.json.DeriveJsonCodec +import zio.json.JsonCodec import org.knora.webapi.slice.admin.domain.service.ProjectExportInfo case class ProjectImportResponse(location: String) object ProjectImportResponse { - implicit val jsonEncoder: JsonEncoder[ProjectImportResponse] = DeriveJsonEncoder.gen[ProjectImportResponse] + implicit val codec: JsonCodec[ProjectImportResponse] = DeriveJsonCodec.gen[ProjectImportResponse] } case class ProjectExportInfoResponse(projectShortname: String, location: String) @@ -20,5 +20,5 @@ object ProjectExportInfoResponse { def apply(info: ProjectExportInfo) = new ProjectExportInfoResponse(info.projectShortname, info.path.toFile.toPath.toAbsolutePath.toString) - implicit val jsonEncoder: JsonEncoder[ProjectExportInfoResponse] = DeriveJsonEncoder.gen[ProjectExportInfoResponse] + implicit val codec: JsonCodec[ProjectExportInfoResponse] = DeriveJsonCodec.gen[ProjectExportInfoResponse] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsEndpointsRequests.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsEndpointsRequests.scala new file mode 100644 index 0000000000..ea8e8ea74c --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsEndpointsRequests.scala @@ -0,0 +1,49 @@ +/* + * 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.slice.admin.api.model + +import zio.json.DeriveJsonCodec +import zio.json.JsonCodec + +import dsp.valueobjects.Iri.ProjectIri +import dsp.valueobjects.Project._ + +object ProjectsEndpointsRequests { + + final case class ProjectCreateRequest( + id: Option[ProjectIri] = None, + shortname: Shortname, + shortcode: Shortcode, + longname: Option[Name] = None, + description: ProjectDescription, + keywords: Keywords, + logo: Option[Logo] = None, + status: ProjectStatus, + selfjoin: ProjectSelfJoin + ) + object ProjectCreateRequest { + implicit val codec: JsonCodec[ProjectCreateRequest] = DeriveJsonCodec.gen[ProjectCreateRequest] + } + + final case class ProjectUpdateRequest( + shortname: Option[Shortname] = None, + longname: Option[Name] = None, + description: Option[ProjectDescription] = None, + keywords: Option[Keywords] = None, + logo: Option[Logo] = None, + status: Option[ProjectStatus] = None, + selfjoin: Option[ProjectSelfJoin] = None + ) + object ProjectUpdateRequest { + implicit val codec: JsonCodec[ProjectUpdateRequest] = DeriveJsonCodec.gen[ProjectUpdateRequest] + } + + final case class ProjectSetRestrictedViewSizeRequest(size: String) + object ProjectSetRestrictedViewSizeRequest { + implicit val codec: JsonCodec[ProjectSetRestrictedViewSizeRequest] = + DeriveJsonCodec.gen[ProjectSetRestrictedViewSizeRequest] + } +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectsADMRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectsADMRestService.scala index d19769cf5d..95a004eb64 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectsADMRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectsADMRestService.scala @@ -11,7 +11,7 @@ import zio.macros.accessible import dsp.errors.BadRequestException import dsp.errors.NotFoundException import dsp.valueobjects.Iri.ProjectIri -import dsp.valueobjects.Project +import dsp.valueobjects.Project.ProjectStatus import dsp.valueobjects.Project.Shortcode import dsp.valueobjects.RestrictedViewSize import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM._ @@ -21,6 +21,9 @@ import org.knora.webapi.responders.admin.ProjectsResponderADM import org.knora.webapi.slice.admin.api.model.ProjectDataGetResponseADM import org.knora.webapi.slice.admin.api.model.ProjectExportInfoResponse import org.knora.webapi.slice.admin.api.model.ProjectImportResponse +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.domain.service.KnoraProjectRepo import org.knora.webapi.slice.admin.domain.service.ProjectExportService import org.knora.webapi.slice.admin.domain.service.ProjectImportService @@ -29,44 +32,46 @@ import org.knora.webapi.slice.common.api.RestPermissionService @accessible trait ProjectADMRestService { - def getProjectsADMRequest(): Task[ProjectsGetResponseADM] - def getSingleProjectADMRequest(identifier: ProjectIdentifierADM): Task[ProjectGetResponseADM] - def createProjectADMRequest( - payload: ProjectCreatePayloadADM, - requestingUser: UserADM - ): Task[ProjectOperationResponseADM] - def deleteProject(projectIri: ProjectIri, requestingUser: UserADM): Task[ProjectOperationResponseADM] + def listAllProjects(): Task[ProjectsGetResponseADM] + + def findProject(id: ProjectIdentifierADM): Task[ProjectGetResponseADM] + + def createProject(createReq: ProjectCreateRequest, user: UserADM): Task[ProjectOperationResponseADM] + def updateProject( - projectIri: ProjectIri, - payload: ProjectUpdatePayloadADM, - requestingUser: UserADM + id: IriIdentifier, + updateReq: ProjectUpdateRequest, + user: UserADM ): Task[ProjectOperationResponseADM] - def getAllProjectData( - iriIdentifier: IriIdentifier, - requestingUser: UserADM - ): Task[ProjectDataGetResponseADM] - def exportProject(shortcode: String, requestingUser: UserADM): Task[Unit] - def importProject(shortcode: String, requestingUser: UserADM): Task[ProjectImportResponse] - def listExports(requestingUser: UserADM): Task[Chunk[ProjectExportInfoResponse]] - def getProjectMembers( - projectIdentifier: ProjectIdentifierADM, - requestingUser: UserADM - ): Task[ProjectMembersGetResponseADM] - def getProjectAdmins( - projectIdentifier: ProjectIdentifierADM, - requestingUser: UserADM - ): Task[ProjectAdminMembersGetResponseADM] - def getKeywords(): Task[ProjectsKeywordsGetResponseADM] - def getKeywordsByProjectIri( - projectIri: ProjectIri - ): Task[ProjectKeywordsGetResponseADM] - def getProjectRestrictedViewSettings( - identifier: ProjectIdentifierADM - ): Task[ProjectRestrictedViewSettingsGetResponseADM] - def setProjectRestrictedViewSettings( + + def deleteProject(id: IriIdentifier, user: UserADM): Task[ProjectOperationResponseADM] + + def getAllProjectData(id: IriIdentifier, user: UserADM): Task[ProjectDataGetResponseADM] + + def exportProject(shortcode: String, user: UserADM): Task[Unit] + def exportProject(id: ShortcodeIdentifier, user: UserADM): Task[Unit] + + def importProject(shortcode: String, user: UserADM): Task[ProjectImportResponse] + + def importProject(shortcode: ShortcodeIdentifier, user: UserADM): Task[ProjectImportResponse] = + importProject(shortcode.value.value, user) + + def listExports(user: UserADM): Task[Chunk[ProjectExportInfoResponse]] + + def getProjectMembers(user: UserADM, id: ProjectIdentifierADM): Task[ProjectMembersGetResponseADM] + + def getProjectAdminMembers(user: UserADM, id: ProjectIdentifierADM): Task[ProjectAdminMembersGetResponseADM] + + def listAllKeywords(): Task[ProjectsKeywordsGetResponseADM] + + def getKeywordsByProjectIri(iri: ProjectIri): Task[ProjectKeywordsGetResponseADM] + + def getProjectRestrictedViewSettings(id: ProjectIdentifierADM): Task[ProjectRestrictedViewSettingsGetResponseADM] + + def updateProjectRestrictedViewSettings( id: ProjectIdentifierADM, user: UserADM, - size: RestrictedViewSize + payload: ProjectSetRestrictedViewSizeRequest ): Task[ProjectRestrictedViewSizeResponseADM] } @@ -86,40 +91,39 @@ final case class ProjectsADMRestServiceLive( * * '''failure''': [[dsp.errors.NotFoundException]] when no project was found */ - def getProjectsADMRequest(): Task[ProjectsGetResponseADM] = + def listAllProjects(): Task[ProjectsGetResponseADM] = responder.projectsGetRequestADM(withSystemProjects = false) /** * Finds the project by its [[ProjectIdentifierADM]] and returns the information as a [[ProjectGetResponseADM]]. * - * @param identifier a [[ProjectIdentifierADM]] instance + * @param id a [[ProjectIdentifierADM]] instance * @return * '''success''': information about the project as a [[ProjectGetResponseADM]] * * '''failure''': [[dsp.errors.NotFoundException]] when no project for the given [[ProjectIdentifierADM]] can be found */ - def getSingleProjectADMRequest(identifier: ProjectIdentifierADM): Task[ProjectGetResponseADM] = - responder.getSingleProjectADMRequest(identifier) + def findProject(id: ProjectIdentifierADM): Task[ProjectGetResponseADM] = responder.getSingleProjectADMRequest(id) /** * Creates a project from the given payload. * - * @param payload the [[ProjectCreatePayloadADM]] from which to create the project - * @param user the [[UserADM]] making the request + * @param createReq the [[ProjectCreateRequest]] from which to create the project + * @param user the [[UserADM]] making the request * @return * '''success''': information about the created project as a [[ProjectOperationResponseADM]] * * '''failure''': [[dsp.errors.NotFoundException]] when no project for the given [[ProjectIri]] - * can be found, if one was provided with the [[ProjectCreatePayloadADM]] + * can be found, if one was provided with the [[ProjectCreateRequest]] * [[dsp.errors.ForbiddenException]] when the requesting user is not allowed to perform the operation */ - def createProjectADMRequest(payload: ProjectCreatePayloadADM, user: UserADM): Task[ProjectOperationResponseADM] = - ZIO.random.flatMap(_.nextUUID).flatMap(responder.projectCreateRequestADM(payload, user, _)) + def createProject(createReq: ProjectCreateRequest, user: UserADM): Task[ProjectOperationResponseADM] = + ZIO.random.flatMap(_.nextUUID).flatMap(responder.projectCreateRequestADM(createReq, user, _)) /** * Deletes the project by its [[ProjectIri]]. * - * @param projectIri the [[ProjectIri]] of the project + * @param id the [[ProjectIri]] of the project * @param user the [[UserADM]] making the request * @return * '''success''': a [[ProjectOperationResponseADM]] @@ -127,29 +131,19 @@ final case class ProjectsADMRestServiceLive( * '''failure''': [[dsp.errors.NotFoundException]] when no project for the given [[ProjectIri]] can be found * [[dsp.errors.ForbiddenException]] when the requesting user is not allowed to perform the operation */ - def deleteProject(projectIri: ProjectIri, user: UserADM): Task[ProjectOperationResponseADM] = + def deleteProject(id: IriIdentifier, user: UserADM): Task[ProjectOperationResponseADM] = { + val updatePayload = ProjectUpdateRequest(status = Some(ProjectStatus.deleted)) for { - projectStatus <- - Project.ProjectStatus.make(false).toZIO.orElseFail(BadRequestException("Invalid project status.")) - updatePayload = ProjectUpdatePayloadADM(status = Some(projectStatus)) - response <- changeBasicInformationRequestADM(projectIri, updatePayload, user) - } yield response - - private def changeBasicInformationRequestADM( - projectIri: ProjectIri, - payload: ProjectUpdatePayloadADM, - user: UserADM - ): Task[ProjectOperationResponseADM] = - for { - id <- ZIO.random.flatMap(_.nextUUID) - response <- responder.changeBasicInformationRequestADM(projectIri, payload, user, id) + apiId <- Random.nextUUID + response <- responder.changeBasicInformationRequestADM(id.value, updatePayload, user, apiId) } yield response + } /** * Updates a project, identified by its [[ProjectIri]]. * - * @param projectIri the [[ProjectIri]] of the project - * @param payload the [[ProjectUpdatePayloadADM]] + * @param id the [[ProjectIri]] of the project + * @param updateReq the [[ProjectUpdateRequest]] * @param user the [[UserADM]] making the request * @return * '''success''': information about the project as a [[ProjectOperationResponseADM]] @@ -158,12 +152,11 @@ final case class ProjectsADMRestServiceLive( * [[dsp.errors.ForbiddenException]] when the requesting user is not allowed to perform the operation */ def updateProject( - projectIri: ProjectIri, - payload: ProjectUpdatePayloadADM, + id: IriIdentifier, + updateReq: ProjectUpdateRequest, user: UserADM - ): Task[ProjectOperationResponseADM] = for { - response <- changeBasicInformationRequestADM(projectIri, payload, user) - } yield response + ): Task[ProjectOperationResponseADM] = + Random.nextUUID.flatMap(responder.changeBasicInformationRequestADM(id.value, updateReq, user, _)) /** * Returns all data of a specific project, identified by its [[ProjectIri]]. @@ -194,7 +187,7 @@ final case class ProjectsADMRestServiceLive( * '''failure''': [[dsp.errors.NotFoundException]] when no project for the given [[ProjectIdentifierADM]] can be found * [[dsp.errors.ForbiddenException]] when the requesting user is not allowed to perform the operation */ - def getProjectMembers(id: ProjectIdentifierADM, user: UserADM): Task[ProjectMembersGetResponseADM] = + def getProjectMembers(user: UserADM, id: ProjectIdentifierADM): Task[ProjectMembersGetResponseADM] = responder.projectMembersGetRequestADM(id, user) /** @@ -208,7 +201,10 @@ final case class ProjectsADMRestServiceLive( * '''failure''': [[dsp.errors.NotFoundException]] when no project for the given [[ProjectIdentifierADM]] can be found * [[dsp.errors.ForbiddenException]] when the requesting user is not allowed to perform the operation */ - def getProjectAdmins(id: ProjectIdentifierADM, user: UserADM): Task[ProjectAdminMembersGetResponseADM] = + def getProjectAdminMembers( + user: UserADM, + id: ProjectIdentifierADM + ): Task[ProjectAdminMembersGetResponseADM] = responder.projectAdminMembersGetRequestADM(id, user) /** @@ -219,19 +215,19 @@ final case class ProjectsADMRestServiceLive( * * '''failure''': [[dsp.errors.NotFoundException]] when no project was found */ - def getKeywords(): Task[ProjectsKeywordsGetResponseADM] = responder.projectsKeywordsGetRequestADM() + def listAllKeywords(): Task[ProjectsKeywordsGetResponseADM] = responder.projectsKeywordsGetRequestADM() /** * Returns all keywords of a specific project, identified by its [[ProjectIri]]. * - * @param projectIri the [[ProjectIri]] of the project + * @param iri the [[ProjectIri]] of the project * @return * '''success''': ist of all keywords as a [[ProjectKeywordsGetResponseADM]] * * '''failure''': [[dsp.errors.NotFoundException]] when no project for the given [[ProjectIri]] can be found */ - def getKeywordsByProjectIri(projectIri: ProjectIri): Task[ProjectKeywordsGetResponseADM] = - responder.projectKeywordsGetRequestADM(projectIri) + def getKeywordsByProjectIri(iri: ProjectIri): Task[ProjectKeywordsGetResponseADM] = + responder.projectKeywordsGetRequestADM(iri) /** * Returns the restricted view settings of a specific project, identified by its [[ProjectIri]]. @@ -248,49 +244,52 @@ final case class ProjectsADMRestServiceLive( /** * Sets project's restricted view settings. * - * @param id the project's id represented by iri, shortocde or shortname, + * @param id the project's id represented by iri, shortcode or shortname, * @param user requesting user, - * @param size value to be set, + * @param payload value to be set, * @return [[ProjectRestrictedViewSizeResponseADM]]. */ - override def setProjectRestrictedViewSettings( + override def updateProjectRestrictedViewSettings( id: ProjectIdentifierADM, user: UserADM, - size: RestrictedViewSize + payload: ProjectSetRestrictedViewSizeRequest ): Task[ProjectRestrictedViewSizeResponseADM] = for { + size <- ZIO.fromEither(RestrictedViewSize.make(payload.size)).mapError(BadRequestException(_)) project <- projectRepo.findById(id).someOrFail(NotFoundException(s"Project '${getId(id)}' not found.")) _ <- permissionService.ensureSystemOrProjectAdmin(user, project) _ <- projectRepo.setProjectRestrictedViewSize(project, size) } yield ProjectRestrictedViewSizeResponseADM(size) - override def exportProject(shortcodeStr: String, requestingUser: UserADM): Task[Unit] = for { - _ <- permissionService.ensureSystemAdmin(requestingUser) - shortcode <- convertStringToShortcode(shortcodeStr) - project <- projectRepo.findByShortcode(shortcode).someOrFail(NotFoundException(s"Project $shortcode not found.")) - _ <- projectExportService.exportProject(project).logError.forkDaemon + override def exportProject(shortcodeStr: String, user: UserADM): Task[Unit] = + convertStringToShortcodeId(shortcodeStr).flatMap(exportProject(_, user)) + + override def exportProject(id: ShortcodeIdentifier, user: UserADM): Task[Unit] = for { + _ <- permissionService.ensureSystemAdmin(user) + project <- projectRepo.findById(id).someOrFail(NotFoundException(s"Project $id not found.")) + _ <- projectExportService.exportProject(project).logError.forkDaemon } yield () - private def convertStringToShortcode(shortcodeStr: String): IO[BadRequestException, Shortcode] = - Shortcode.make(shortcodeStr).toZIO.mapError(err => BadRequestException(err.msg)) + private def convertStringToShortcodeId(shortcodeStr: String): IO[BadRequestException, ShortcodeIdentifier] = + Shortcode.make(shortcodeStr).toZIO.mapBoth(err => BadRequestException(err.msg), ShortcodeIdentifier.from) override def importProject( shortcodeStr: String, - requestingUser: UserADM + user: UserADM ): Task[ProjectImportResponse] = for { - _ <- permissionService.ensureSystemAdmin(requestingUser) - shortcode <- convertStringToShortcode(shortcodeStr) + _ <- permissionService.ensureSystemAdmin(user) + shortcode <- convertStringToShortcodeId(shortcodeStr) path <- projectImportService - .importProject(shortcode, requestingUser) + .importProject(shortcode.value, user) .flatMap { case Some(export) => export.toAbsolutePath.map(_.toString) case None => ZIO.fail(NotFoundException(s"Project export for ${shortcode.value} not found.")) } } yield ProjectImportResponse(path) - override def listExports(requestingUser: UserADM): Task[Chunk[ProjectExportInfoResponse]] = for { - _ <- permissionService.ensureSystemAdmin(requestingUser) + override def listExports(user: UserADM): Task[Chunk[ProjectExportInfoResponse]] = for { + _ <- permissionService.ensureSystemAdmin(user) exports <- projectExportService.listExports().map(_.map(ProjectExportInfoResponse(_))) } yield exports } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala new file mode 100644 index 0000000000..ad00528fff --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/BaseEndpoints.scala @@ -0,0 +1,93 @@ +/* + * 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.slice.common.api + +import sttp.model.StatusCode +import sttp.model.headers.WWWAuthenticateChallenge +import sttp.tapir.EndpointOutput +import sttp.tapir.auth +import sttp.tapir.cookie +import sttp.tapir.endpoint +import sttp.tapir.generic.auto._ +import sttp.tapir.json.zio.jsonBody +import sttp.tapir.model.UsernamePassword +import sttp.tapir.oneOf +import sttp.tapir.oneOfVariant +import sttp.tapir.statusCode +import zio.ZIO +import zio.ZLayer + +import scala.concurrent.Future + +import dsp.errors._ +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.messages.admin.responder.usersmessages.UserADM +import org.knora.webapi.messages.admin.responder.usersmessages.UserIdentifierADM +import org.knora.webapi.messages.v2.routing.authenticationmessages.KnoraCredentialsV2.KnoraPasswordCredentialsV2 +import org.knora.webapi.routing.Authenticator +import org.knora.webapi.routing.UnsafeZioRun + +final case class BaseEndpoints(authenticator: Authenticator, implicit val r: zio.Runtime[Any]) { + + private val defaultErrorOutputs: EndpointOutput.OneOf[RequestRejectedException, RequestRejectedException] = + oneOf[RequestRejectedException]( + oneOfVariant[NotFoundException](statusCode(StatusCode.NotFound).and(jsonBody[NotFoundException])), + oneOfVariant[BadRequestException](statusCode(StatusCode.BadRequest).and(jsonBody[BadRequestException])), + oneOfVariant[ValidationException](statusCode(StatusCode.BadRequest).and(jsonBody[ValidationException])), + oneOfVariant[DuplicateValueException](statusCode(StatusCode.BadRequest).and(jsonBody[DuplicateValueException])) + ) + + private val secureDefaultErrorOutputs: EndpointOutput.OneOf[RequestRejectedException, RequestRejectedException] = + oneOf[RequestRejectedException]( + // default + oneOfVariant[NotFoundException](statusCode(StatusCode.NotFound).and(jsonBody[NotFoundException])), + oneOfVariant[BadRequestException](statusCode(StatusCode.BadRequest).and(jsonBody[BadRequestException])), + oneOfVariant[ValidationException](statusCode(StatusCode.BadRequest).and(jsonBody[ValidationException])), + oneOfVariant[DuplicateValueException](statusCode(StatusCode.BadRequest).and(jsonBody[DuplicateValueException])), + // plus security + oneOfVariant[BadCredentialsException](statusCode(StatusCode.Unauthorized).and(jsonBody[BadCredentialsException])), + oneOfVariant[ForbiddenException](statusCode(StatusCode.Forbidden).and(jsonBody[ForbiddenException])) + ) + + val publicEndpoint = endpoint.errorOut(defaultErrorOutputs) + + val securedEndpoint = endpoint + .errorOut(secureDefaultErrorOutputs) + .securityIn(auth.bearer[Option[String]](WWWAuthenticateChallenge.bearer)) + .securityIn(cookie[Option[String]](authenticator.calculateCookieName())) + .securityIn(auth.basic[Option[UsernamePassword]](WWWAuthenticateChallenge.basic("realm"))) + .serverSecurityLogic { + case (Some(jwtToken), _, _) => authenticateJwt(jwtToken) + case (_, Some(cookie), _) => authenticateJwt(cookie) + case (_, _, Some(basic)) => authenticateBasic(basic) + case _ => Future.successful(Left(BadCredentialsException("No credentials provided."))) + } + + private def authenticateJwt(jwtToken: String): Future[Either[RequestRejectedException, UserADM]] = + UnsafeZioRun.runToFuture( + authenticator.verifyJwt(jwtToken).refineOrDie { case e: RequestRejectedException => e }.either + ) + + private def authenticateBasic(basic: UsernamePassword): Future[Either[RequestRejectedException, UserADM]] = + UnsafeZioRun.runToFuture( + ZIO + .attempt(UserIdentifierADM(maybeEmail = Some(basic.username))(StringFormatter.getGeneralInstance)) + .map(id => Some(KnoraPasswordCredentialsV2(id, basic.password.getOrElse("")))) + .flatMap(authenticator.getUserADMThroughCredentialsV2) + .orElseFail(BadCredentialsException("Invalid credentials.")) + .refineOrDie { case e: RequestRejectedException => e } + .either + ) +} + +object BaseEndpoints { + val layer = ZLayer.fromZIO( + for { + auth <- ZIO.service[Authenticator] + r <- ZIO.runtime[Any] + } yield BaseEndpoints(auth, r) + ) +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/HandlerMapper.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/HandlerMapper.scala new file mode 100644 index 0000000000..f59b494e62 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/HandlerMapper.scala @@ -0,0 +1,63 @@ +/* + * 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.slice.common.api + +import sttp.tapir.Endpoint +import sttp.tapir.model.UsernamePassword +import sttp.tapir.server.PartialServerEndpoint +import sttp.tapir.server.ServerEndpoint.Full +import zio.Task +import zio.ZIO +import zio.ZLayer + +import scala.concurrent.Future + +import dsp.errors.RequestRejectedException +import org.knora.webapi.messages.admin.responder.usersmessages.UserADM +import org.knora.webapi.routing.UnsafeZioRun +import org.knora.webapi.slice.common.api.InputType.SecurityIn + +object InputType { + type SecurityIn = (Option[String], Option[String], Option[UsernamePassword]) +} + +case class EndpointAndZioHandler[SECURITY_INPUT, INPUT, OUTPUT]( + endpoint: Endpoint[SECURITY_INPUT, INPUT, RequestRejectedException, OUTPUT, Any], + handler: INPUT => Task[OUTPUT] +) + +case class SecuredEndpointAndZioHandler[INPUT, OUTPUT]( + endpoint: PartialServerEndpoint[ + SecurityIn, + UserADM, + INPUT, + RequestRejectedException, + OUTPUT, + Any, + Future + ], + handler: UserADM => INPUT => Task[OUTPUT] +) + +final case class HandlerMapperF()(implicit val r: zio.Runtime[Any]) { + + def mapEndpointAndHandler[INPUT, OUTPUT]( + handlerAndEndpoint: SecuredEndpointAndZioHandler[INPUT, OUTPUT] + ): Full[SecurityIn, UserADM, INPUT, RequestRejectedException, OUTPUT, Any, Future] = + handlerAndEndpoint.endpoint.serverLogic(user => in => { runToFuture(handlerAndEndpoint.handler(user)(in)) }) + + def mapEndpointAndHandler[INPUT, OUTPUT]( + handlerAndEndpoint: EndpointAndZioHandler[Unit, INPUT, OUTPUT] + ): Full[Unit, Unit, INPUT, RequestRejectedException, OUTPUT, Any, Future] = + handlerAndEndpoint.endpoint.serverLogic[Future](in => runToFuture(handlerAndEndpoint.handler(in))) + + def runToFuture[OUTPUT](zio: Task[OUTPUT]): Future[Either[RequestRejectedException, OUTPUT]] = + UnsafeZioRun.runToFuture(zio.refineOrDie { case e: RequestRejectedException => e }.either) +} + +object HandlerMapperF { + val layer = ZLayer.fromZIO(ZIO.runtime[Any].map(HandlerMapperF()(_))) +} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/TapirToPekkoInterpreter.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/TapirToPekkoInterpreter.scala similarity index 94% rename from webapi/src/main/scala/org/knora/webapi/routing/TapirToPekkoInterpreter.scala rename to webapi/src/main/scala/org/knora/webapi/slice/common/api/TapirToPekkoInterpreter.scala index f3b5627618..e0d8574fcf 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/TapirToPekkoInterpreter.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/TapirToPekkoInterpreter.scala @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package org.knora.webapi.routing +package org.knora.webapi.slice.common.api import org.apache.pekko.http.scaladsl.server.Route import sttp.capabilities.WebSockets @@ -38,5 +38,6 @@ final case class TapirToPekkoInterpreter()(implicit executionContext: ExecutionC private val interpreter: PekkoHttpServerInterpreter = PekkoHttpServerInterpreter(serverOptions) - def toRoute(endpoint: ServerEndpoint[PekkoStreams with WebSockets, Future]): Route = interpreter.toRoute(endpoint) + def toRoute(endpoint: ServerEndpoint[PekkoStreams with WebSockets, Future]): Route = + interpreter.toRoute(endpoint) } diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectADMRestServiceMock.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectADMRestServiceMock.scala index 2a202abc02..e6202344a6 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectADMRestServiceMock.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectADMRestServiceMock.scala @@ -10,23 +10,26 @@ import zio._ import zio.mock._ import dsp.valueobjects.Iri._ -import dsp.valueobjects.RestrictedViewSize +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.IriIdentifier +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.ShortcodeIdentifier import org.knora.webapi.messages.admin.responder.projectsmessages._ import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.slice.admin.api.model.ProjectDataGetResponseADM import org.knora.webapi.slice.admin.api.model.ProjectExportInfoResponse import org.knora.webapi.slice.admin.api.model.ProjectImportResponse +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 object ProjectADMRestServiceMock extends Mock[ProjectADMRestService] { object GetProjects extends Effect[Unit, Throwable, ProjectsGetResponseADM] object GetSingleProject extends Effect[ProjectIdentifierADM, Throwable, ProjectGetResponseADM] - object CreateProject extends Effect[(ProjectCreatePayloadADM, UserADM), Throwable, ProjectOperationResponseADM] - object DeleteProject extends Effect[(ProjectIri, UserADM), Throwable, ProjectOperationResponseADM] + object CreateProject extends Effect[(ProjectCreateRequest, UserADM), Throwable, ProjectOperationResponseADM] + object DeleteProject extends Effect[(IriIdentifier, UserADM), Throwable, ProjectOperationResponseADM] object UpdateProject - extends Effect[(ProjectIri, ProjectUpdatePayloadADM, UserADM), Throwable, ProjectOperationResponseADM] - object GetAllProjectData - extends Effect[(ProjectIdentifierADM.IriIdentifier, UserADM), Throwable, ProjectDataGetResponseADM] + extends Effect[(IriIdentifier, ProjectUpdateRequest, UserADM), Throwable, ProjectOperationResponseADM] + object GetAllProjectData extends Effect[(IriIdentifier, UserADM), Throwable, ProjectDataGetResponseADM] object GetProjectMembers extends Effect[(ProjectIdentifierADM, UserADM), Throwable, ProjectMembersGetResponseADM] object GetProjectAdmins extends Effect[(ProjectIdentifierADM, UserADM), Throwable, ProjectAdminMembersGetResponseADM] object GetKeywords extends Effect[Unit, Throwable, ProjectsKeywordsGetResponseADM] @@ -40,47 +43,50 @@ object ProjectADMRestServiceMock extends Mock[ProjectADMRestService] { proxy <- ZIO.service[Proxy] } yield new ProjectADMRestService { - def getProjectsADMRequest(): Task[ProjectsGetResponseADM] = + def listAllProjects(): Task[ProjectsGetResponseADM] = proxy(GetProjects) - def getSingleProjectADMRequest(identifier: ProjectIdentifierADM): Task[ProjectGetResponseADM] = + def findProject(identifier: ProjectIdentifierADM): Task[ProjectGetResponseADM] = proxy(GetSingleProject, identifier) - def createProjectADMRequest( - payload: ProjectCreatePayloadADM, + def createProject( + createReq: ProjectCreateRequest, requestingUser: UserADM ): Task[ProjectOperationResponseADM] = - proxy(CreateProject, (payload, requestingUser)) + proxy(CreateProject, (createReq, requestingUser)) - def deleteProject(iri: ProjectIri, requestingUser: UserADM): Task[ProjectOperationResponseADM] = - proxy(DeleteProject, (iri, requestingUser)) + def deleteProject( + id: ProjectIdentifierADM.IriIdentifier, + requestingUser: UserADM + ): Task[ProjectOperationResponseADM] = + proxy(DeleteProject, (id, requestingUser)) def updateProject( - projectIri: ProjectIri, - payload: ProjectUpdatePayloadADM, + id: IriIdentifier, + updateReq: ProjectUpdateRequest, requestingUser: UserADM ): Task[ProjectOperationResponseADM] = - proxy(UpdateProject, (projectIri, payload, requestingUser)) + proxy(UpdateProject, (id, updateReq, requestingUser)) def getAllProjectData( - iri: ProjectIdentifierADM.IriIdentifier, - requestingUser: UserADM + id: ProjectIdentifierADM.IriIdentifier, + user: UserADM ): Task[ProjectDataGetResponseADM] = - proxy(GetAllProjectData, (iri, requestingUser)) + proxy(GetAllProjectData, (id, user)) def getProjectMembers( - identifier: ProjectIdentifierADM, - requestingUser: UserADM + requestingUser: UserADM, + identifier: ProjectIdentifierADM ): Task[ProjectMembersGetResponseADM] = proxy(GetProjectMembers, (identifier, requestingUser)) - def getProjectAdmins( - identifier: ProjectIdentifierADM, - requestingUser: UserADM + def getProjectAdminMembers( + requestingUser: UserADM, + identifier: ProjectIdentifierADM ): Task[ProjectAdminMembersGetResponseADM] = proxy(GetProjectAdmins, (identifier, requestingUser)) - def getKeywords(): Task[ProjectsKeywordsGetResponseADM] = + def listAllKeywords(): Task[ProjectsKeywordsGetResponseADM] = proxy(GetKeywords) def getKeywordsByProjectIri( @@ -95,14 +101,16 @@ object ProjectADMRestServiceMock extends Mock[ProjectADMRestService] { override def exportProject(projectIri: IRI, requestingUser: UserADM): Task[Unit] = ??? + override def exportProject(shortcode: ShortcodeIdentifier, requestingUser: UserADM): Task[Unit] = ??? + override def importProject(projectIri: IRI, requestingUser: UserADM): Task[ProjectImportResponse] = ??? override def listExports(requestingUser: UserADM): Task[Chunk[ProjectExportInfoResponse]] = ??? - override def setProjectRestrictedViewSettings( + override def updateProjectRestrictedViewSettings( id: ProjectIdentifierADM, user: UserADM, - size: RestrictedViewSize + size: ProjectSetRestrictedViewSizeRequest ): Task[ProjectRestrictedViewSizeResponseADM] = ??? } } diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMMock.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMMock.scala index b425279898..6abae540ce 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMMock.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMMock.scala @@ -18,6 +18,8 @@ import java.util.UUID import dsp.valueobjects.Iri import org.knora.webapi.messages.admin.responder.projectsmessages._ import org.knora.webapi.messages.admin.responder.usersmessages.UserADM +import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectCreateRequest +import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectUpdateRequest object ProjectsResponderADMMock extends Mock[ProjectsResponderADM] { @@ -35,9 +37,9 @@ object ProjectsResponderADMMock extends Mock[ProjectsResponderADM] { object ProjectRestrictedViewSettingsGetRequestADM extends Effect[ProjectIdentifierADM, Throwable, ProjectRestrictedViewSettingsGetResponseADM] object ProjectCreateRequestADM - extends Effect[(ProjectCreatePayloadADM, UserADM, UUID), Throwable, ProjectOperationResponseADM] + extends Effect[(ProjectCreateRequest, UserADM, UUID), Throwable, ProjectOperationResponseADM] object ChangeBasicInformationRequestADM - extends Effect[(Iri.ProjectIri, ProjectUpdatePayloadADM, UserADM, UUID), Throwable, ProjectOperationResponseADM] + extends Effect[(Iri.ProjectIri, ProjectUpdateRequest, UserADM, UUID), Throwable, ProjectOperationResponseADM] val compose: URLayer[mock.Proxy, ProjectsResponderADM] = ZLayer { @@ -73,14 +75,14 @@ object ProjectsResponderADMMock extends Mock[ProjectsResponderADM] { ): Task[ProjectRestrictedViewSettingsGetResponseADM] = proxy(ProjectRestrictedViewSettingsGetRequestADM, id) override def projectCreateRequestADM( - createPayload: ProjectCreatePayloadADM, + createPayload: ProjectCreateRequest, requestingUser: UserADM, apiRequestID: UUID ): Task[ProjectOperationResponseADM] = proxy(ProjectCreateRequestADM, (createPayload, requestingUser, apiRequestID)) override def changeBasicInformationRequestADM( projectIri: Iri.ProjectIri, - updatePayload: ProjectUpdatePayloadADM, + updatePayload: ProjectUpdateRequest, user: UserADM, apiRequestID: UUID ): Task[ProjectOperationResponseADM] = diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceLiveSpec.scala index c2ae32e209..e72460cf69 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceLiveSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceLiveSpec.scala @@ -13,9 +13,12 @@ import dsp.valueobjects.V2._ import org.knora.webapi.TestDataFactory import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.IriIdentifier import org.knora.webapi.messages.admin.responder.projectsmessages._ import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.util.KnoraSystemInstances.Users.SystemUser +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 import org.knora.webapi.slice.admin.api.service.ProjectsADMRestServiceLive import org.knora.webapi.slice.admin.domain.service.DspIngestClientMock @@ -76,7 +79,7 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault { val expectedResponse = ProjectsGetResponseADM(Seq(projectADM)) val mockResponder = ProjectsResponderADMMock.ProjectsGetRequestADM(Expectation.value(expectedResponse)) for { - _ <- ProjectADMRestService.getProjectsADMRequest().provide(projectServiceLayer(mockResponder)) + _ <- ProjectADMRestService.listAllProjects().provide(projectServiceLayer(mockResponder)) } yield assertCompletes } @@ -89,7 +92,7 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault { result = Expectation.value(ProjectGetResponseADM(projectADM)) ) for { - _ <- ProjectADMRestService.getSingleProjectADMRequest(identifier).provide(projectServiceLayer(mockResponder)) + _ <- ProjectADMRestService.findProject(identifier).provide(projectServiceLayer(mockResponder)) } yield assertCompletes }, test("get project by shortname") { @@ -100,7 +103,7 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault { result = Expectation.value(ProjectGetResponseADM(projectADM)) ) for { - _ <- ProjectADMRestService.getSingleProjectADMRequest(identifier).provide(projectServiceLayer(mockResponder)) + _ <- ProjectADMRestService.findProject(identifier).provide(projectServiceLayer(mockResponder)) } yield assertCompletes }, test("get project by shortcode") { @@ -111,13 +114,13 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault { result = Expectation.value(ProjectGetResponseADM(projectADM)) ) for { - _ <- ProjectADMRestService.getSingleProjectADMRequest(identifier).provide(projectServiceLayer(mockResponder)) + _ <- ProjectADMRestService.findProject(identifier).provide(projectServiceLayer(mockResponder)) } yield assertCompletes } ) val createProjectSpec: Spec[Any, Throwable] = test("create a project") { - val payload = ProjectCreatePayloadADM( + val payload = ProjectCreateRequest( None, TestDataFactory.projectShortname("newproject"), TestDataFactory.projectShortcode("3333"), @@ -138,7 +141,7 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault { result = Expectation.value(ProjectOperationResponseADM(projectADM)) ) _ <- - ProjectADMRestService.createProjectADMRequest(payload, SystemUser).provide(projectServiceLayer(mockResponder)) + ProjectADMRestService.createProject(payload, SystemUser).provide(projectServiceLayer(mockResponder)) } yield assertCompletes } @@ -147,7 +150,7 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault { val iri = "http://rdfh.ch/projects/0001" val projectIri = TestDataFactory.projectIri(iri) val projectStatus = Some(TestDataFactory.projectStatus(false)) - val projectUpdatePayload = ProjectUpdatePayloadADM(status = projectStatus) + val projectUpdatePayload = ProjectUpdateRequest(status = projectStatus) for { uuid <- ZIO.random.flatMap(_.nextUUID) _ <- TestRandom.feedUUIDs(uuid) @@ -155,14 +158,16 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault { assertion = Assertion.equalTo(projectIri, projectUpdatePayload, SystemUser, uuid), result = Expectation.value(ProjectOperationResponseADM(projectADM)) ) - _ <- ProjectADMRestService.deleteProject(projectIri, SystemUser).provide(projectServiceLayer(mockResponder)) + _ <- ProjectADMRestService + .deleteProject(IriIdentifier.from(projectIri), SystemUser) + .provide(projectServiceLayer(mockResponder)) } yield assertCompletes } val updateProjectSpec: Spec[Any, Throwable] = test("update a project") { val iri = "http://rdfh.ch/projects/0001" val projectIri = TestDataFactory.projectIri(iri) - val projectUpdatePayload = ProjectUpdatePayloadADM( + val projectUpdatePayload = ProjectUpdateRequest( Some(TestDataFactory.projectShortname("usn")), Some(TestDataFactory.projectName("updated project longname")), Some(TestDataFactory.projectDescription(Seq(StringLiteralV2("updated project description", Some("en"))))), @@ -179,7 +184,7 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault { result = Expectation.value(ProjectOperationResponseADM(projectADM)) ) _ <- ProjectADMRestService - .updateProject(projectIri, projectUpdatePayload, SystemUser) + .updateProject(IriIdentifier.from(projectIri), projectUpdatePayload, SystemUser) .provide(projectServiceLayer(mockResponder)) } yield assertCompletes } @@ -194,7 +199,7 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault { ) for { _ <- ProjectADMRestService - .getProjectMembers(identifier, SystemUser) + .getProjectMembers(SystemUser, identifier) .provide(projectServiceLayer(mockResponder)) } yield assertCompletes }, @@ -207,7 +212,7 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault { ) for { _ <- ProjectADMRestService - .getProjectMembers(identifier, SystemUser) + .getProjectMembers(SystemUser, identifier) .provide(projectServiceLayer(mockResponder)) } yield assertCompletes }, @@ -220,7 +225,7 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault { ) for { _ <- ProjectADMRestService - .getProjectMembers(identifier, SystemUser) + .getProjectMembers(SystemUser, identifier) .provide(projectServiceLayer(mockResponder)) } yield assertCompletes } @@ -236,7 +241,7 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault { ) for { _ <- ProjectADMRestService - .getProjectAdmins(identifier, SystemUser) + .getProjectAdminMembers(SystemUser, identifier) .provide(projectServiceLayer(mockResponder)) } yield assertCompletes }, @@ -249,7 +254,7 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault { ) for { _ <- ProjectADMRestService - .getProjectAdmins(identifier, SystemUser) + .getProjectAdminMembers(SystemUser, identifier) .provide(projectServiceLayer(mockResponder)) } yield assertCompletes }, @@ -262,7 +267,7 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault { ) for { _ <- ProjectADMRestService - .getProjectAdmins(identifier, SystemUser) + .getProjectAdminMembers(SystemUser, identifier) .provide(projectServiceLayer(mockResponder)) } yield assertCompletes } @@ -274,7 +279,7 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault { ) for { _ <- ProjectADMRestService - .getKeywords() + .listAllKeywords() .provide(projectServiceLayer(mockResponder)) } yield assertCompletes } 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 index 162ee80bc2..1aa88dc2af 100644 --- a/webapi/src/test/scala/org/knora/webapi/routing/admin/ProjectsRouteZSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/routing/admin/ProjectsRouteZSpec.scala @@ -18,6 +18,7 @@ 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._ @@ -25,6 +26,8 @@ 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 { @@ -211,7 +214,7 @@ object ProjectsRouteZSpec extends ZIOSpecDefault { val status = TestDataFactory.projectStatus(true) val selfJoin = TestDataFactory.projectSelfJoin(false) - val projectCreatePayload = ProjectCreatePayloadADM( + val projectCreatePayload = ProjectCreateRequest( id = None, shortname = shortname, shortcode = shortcode, @@ -285,7 +288,7 @@ object ProjectsRouteZSpec extends ZIOSpecDefault { val expectedResult = Expectation.value[ProjectOperationResponseADM](ProjectOperationResponseADM(getProjectADM())) val mockService: ULayer[ProjectADMRestService] = ProjectADMRestServiceMock .DeleteProject( - assertion = Assertion.equalTo(projectIri, user), + assertion = Assertion.equalTo(IriIdentifier.from(projectIri), user), result = expectedResult ) .toLayer @@ -318,7 +321,7 @@ object ProjectsRouteZSpec extends ZIOSpecDefault { val projectStatus = TestDataFactory.projectStatus(true) val selfJoin = TestDataFactory.projectSelfJoin(true) - val projectUpdatePayload = ProjectUpdatePayloadADM( + val projectUpdatePayload = ProjectUpdateRequest( shortname = Some(updatedShortname), longname = Some(updatedLongname), description = Some(updatedDescription), @@ -347,7 +350,7 @@ object ProjectsRouteZSpec extends ZIOSpecDefault { val expectedResult = Expectation.value[ProjectOperationResponseADM](ProjectOperationResponseADM(getProjectADM())) val mockService = ProjectADMRestServiceMock .UpdateProject( - assertion = Assertion.equalTo((projectIri, projectUpdatePayload, user)), + assertion = Assertion.equalTo((IriIdentifier.from(projectIri), projectUpdatePayload, user)), result = expectedResult ) .toLayer