Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Migrate secure admin/projects endpoints to Tapir DEV-2806 #2872

Merged
merged 36 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
26df5d0
refactor: Introduce tapir on Akka
seakayone Oct 5, 2023
0401dc6
Turn error response to Json
seakayone Oct 5, 2023
081443a
Turn error response to Json and add metrics
seakayone Oct 5, 2023
9820928
Extract interpreter
seakayone Oct 5, 2023
3485a1d
add secure endpoints (wip: only bearer for now)
seakayone Oct 5, 2023
c3357d4
Add cookie auth
seakayone Oct 5, 2023
34777ec
Add basic auth
seakayone Oct 5, 2023
2302181
add admin members endpoint
seakayone Oct 5, 2023
fe42d52
add delete project endpoint
seakayone Oct 5, 2023
0715844
add delete project endpoint
seakayone Oct 5, 2023
e68b143
add export and create project endpoint
seakayone Oct 6, 2023
54b10b4
add update project route
seakayone Oct 6, 2023
47cd2d8
clarify ProjectsRestService methods naming
seakayone Oct 6, 2023
5dd43cc
fix test
seakayone Oct 6, 2023
acfeb77
rm index.html
seakayone Oct 6, 2023
e5e0467
Merge branch 'main' into chore/add-tapir-dependencies
seakayone Oct 6, 2023
d6456f3
Merge branch 'chore/add-tapir-dependencies' into refactor/add-secure-…
seakayone Oct 6, 2023
290e112
add admin/projects/export endpoint
seakayone Oct 6, 2023
68b7767
add set restricted view size endpoints to tapir/pekko
seakayone Oct 6, 2023
18716ae
add copyright header
seakayone Oct 6, 2023
74a5496
Merge branch 'chore/add-tapir-dependencies' into refactor/add-secure-…
seakayone Oct 6, 2023
281fea6
add post import endpoint
seakayone Oct 6, 2023
bcd49f2
cleanup
seakayone Oct 6, 2023
b6abc25
cleanup
seakayone Oct 6, 2023
fcc4a6f
cleanup, remove duplicate "Payload/Request" objects
seakayone Oct 6, 2023
0657874
Merge branch 'main' into chore/add-tapir-dependencies
seakayone Oct 11, 2023
95d48c8
Rename it to handler
seakayone Oct 11, 2023
d80861a
Merge branch 'chore/add-tapir-dependencies' into refactor/add-secure-…
seakayone Oct 11, 2023
893a17e
move ProjectEndpoints to admin.api package
seakayone Oct 11, 2023
7fd19ff
move BaseEndpoints to common.api package
seakayone Oct 11, 2023
ea54e56
move HandlerMapper and TapirToPekkoInterpreter to common.api package
seakayone Oct 11, 2023
638e47e
move HandlerMapper and TapirToPekkoInterpreter to common.api package
seakayone Oct 11, 2023
cd8b4be
fmt
seakayone Oct 11, 2023
e1f72ea
Merge branch 'main' into refactor/add-secure-endpoints
seakayone Oct 12, 2023
9ed8a02
Update documentation
seakayone Oct 12, 2023
1759a94
Move all projects endpoints requests to admin.api.model package
seakayone Oct 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@ import org.knora.webapi.responders.v2.ontology.OntologyHelpersLive
import org.knora.webapi.routing._
import org.knora.webapi.routing.admin.AuthenticatorService
import org.knora.webapi.routing.admin.ProjectsRouteZ
import org.knora.webapi.slice.admin.api.ProjectsEndpoints
import org.knora.webapi.slice.admin.api.ProjectsEndpointsHandlerF
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
Expand Down Expand Up @@ -131,11 +135,13 @@ object LayersTest {

private val commonLayersForAllIntegrationTests =
ZLayer.makeSome[CommonR0, CommonR](
HandlerMapperF.layer,
ApiRoutes.layer,
AppRouter.layer,
AuthenticationMiddleware.layer,
AuthenticatorLive.layer,
AuthenticatorService.layer,
BaseEndpoints.layer,
CacheServiceInMemImpl.layer,
CacheServiceRequestMessageHandlerLive.layer,
CardinalityHandlerLive.layer,
Expand All @@ -157,6 +163,7 @@ object LayersTest {
MessageRelayLive.layer,
OntologyCacheLive.layer,
OntologyHelpersLive.layer,
OntologyInferencer.layer,
OntologyRepoLive.layer,
OntologyResponderV2Live.layer,
PermissionUtilADMLive.layer,
Expand All @@ -168,6 +175,8 @@ object LayersTest {
ProjectExportStorageServiceLive.layer,
ProjectImportServiceLive.layer,
ProjectsADMRestServiceLive.layer,
ProjectsEndpoints.layer,
ProjectsEndpointsHandlerF.layer,
ProjectsResponderADMLive.layer,
ProjectsRouteZ.layer,
QueryTraverser.layer,
Expand All @@ -181,7 +190,6 @@ object LayersTest {
RestResourceInfoService.layer,
SearchResponderV2Live.layer,
SipiResponderADMLive.layer,
OntologyInferencer.layer,
StandoffResponderV2Live.layer,
StandoffTagUtilV2Live.layer,
State.layer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"}"""
Expand Down Expand Up @@ -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"}"""
Expand Down Expand Up @@ -872,6 +871,5 @@ class ProjectsADME2EZioHttpSpec extends E2ESpec with ProjectsADMJsonProtocol {
assert(response.status === StatusCodes.Forbidden)
}
}
else "used to set RestrictedViewSize by project Shortcode" ignore ()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,13 @@ 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

/**
* 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](
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,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),
Expand Down Expand Up @@ -259,7 +259,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)),
Expand Down Expand Up @@ -291,7 +291,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),
Expand Down Expand Up @@ -323,7 +323,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),
Expand All @@ -343,7 +343,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),
Expand Down Expand Up @@ -374,7 +374,7 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender {

appActor ! ProjectChangeRequestADM(
projectIri = iri,
projectUpdatePayload = ProjectUpdatePayloadADM(
projectUpdatePayload = ProjectUpdateRequest(
shortname = None,
longname = Some(updatedLongname),
description = Some(updatedDescription),
Expand Down Expand Up @@ -409,7 +409,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()
)
Expand All @@ -421,12 +421,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 {
Expand Down
50 changes: 32 additions & 18 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ package org.knora

import sbt.*

import scala.collection.immutable.Seq

object Dependencies {

val fusekiImage =
Expand All @@ -30,21 +32,19 @@ object Dependencies {
val ZioVersion = "2.0.18"

// ZIO - all Scala 3 compatible
val zio = "dev.zio" %% "zio" % ZioVersion
val zioConfig = "dev.zio" %% "zio-config" % ZioConfigVersion
val zioConfigMagnolia = "dev.zio" %% "zio-config-magnolia" % ZioConfigVersion
val zioConfigTypesafe = "dev.zio" %% "zio-config-typesafe" % ZioConfigVersion
val zioHttpOld = "io.d11" %% "zhttp" % ZioHttpVersionOld
val zioHttp = "dev.zio" %% "zio-http" % ZioHttpVersion
val zioJson = "dev.zio" %% "zio-json" % "0.6.2"
val zioLogging = "dev.zio" %% "zio-logging" % ZioLoggingVersion
val zioLoggingSlf4jBridge = "dev.zio" %% "zio-logging-slf4j2-bridge" % ZioLoggingVersion
val zioNio = "dev.zio" %% "zio-nio" % ZioNioVersion
val zioMacros = "dev.zio" %% "zio-macros" % ZioVersion
val zioMetricsConnectors = "dev.zio" %% "zio-metrics-connectors" % ZioMetricsConnectorsVersion
val zioMetricsPrometheusConnector = "dev.zio" %% "zio-metrics-connectors-prometheus" % ZioMetricsConnectorsVersion
val zioPrelude = "dev.zio" %% "zio-prelude" % ZioPreludeVersion
val zioSttp = "com.softwaremill.sttp.client3" %% "zio" % "3.9.0"
val zio = "dev.zio" %% "zio" % ZioVersion
val zioConfig = "dev.zio" %% "zio-config" % ZioConfigVersion
val zioConfigMagnolia = "dev.zio" %% "zio-config-magnolia" % ZioConfigVersion
val zioConfigTypesafe = "dev.zio" %% "zio-config-typesafe" % ZioConfigVersion
val zioHttpOld = "io.d11" %% "zhttp" % ZioHttpVersionOld
val zioHttp = "dev.zio" %% "zio-http" % ZioHttpVersion
val zioJson = "dev.zio" %% "zio-json" % "0.6.2"
val zioLogging = "dev.zio" %% "zio-logging" % ZioLoggingVersion
val zioLoggingSlf4jBridge = "dev.zio" %% "zio-logging-slf4j2-bridge" % ZioLoggingVersion
val zioNio = "dev.zio" %% "zio-nio" % ZioNioVersion
val zioMacros = "dev.zio" %% "zio-macros" % ZioVersion
val zioPrelude = "dev.zio" %% "zio-prelude" % ZioPreludeVersion
val zioSttp = "com.softwaremill.sttp.client3" %% "zio" % "3.9.0"

// refined
val refined = Seq(
Expand Down Expand Up @@ -125,6 +125,22 @@ object Dependencies {
// found/added by the plugin but deleted anyway
val commonsLang3 = "org.apache.commons" % "commons-lang3" % "3.13.0"

val tapirVersion = "1.7.6"

val tapir = Seq(
"com.softwaremill.sttp.tapir" %% "tapir-pekko-http-server" % tapirVersion,
// "com.softwaremill.sttp.tapir" %% "tapir-zio-http-server" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-json-zio" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-json-spray" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-refined" % "1.2.10"
)
val metrics = Seq(
"dev.zio" %% "zio-metrics-connectors" % ZioMetricsConnectorsVersion,
"dev.zio" %% "zio-metrics-connectors-prometheus" % ZioMetricsConnectorsVersion,
"com.softwaremill.sttp.tapir" %% "tapir-zio-metrics" % tapirVersion
)

val integrationTestDependencies = Seq(
pekkoHttpTestkit,
pekkoStreamTestkit,
Expand Down Expand Up @@ -178,10 +194,8 @@ object Dependencies {
zioLogging,
zioLoggingSlf4jBridge,
zioMacros,
zioMetricsConnectors,
zioMetricsPrometheusConnector,
zioNio,
zioPrelude,
zioSttp
)
) ++ metrics ++ tapir
}
26 changes: 24 additions & 2 deletions webapi/src/main/scala/dsp/errors/Errors.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ package dsp.errors
import com.typesafe.scalalogging.Logger
import org.apache.commons.lang3.SerializationException
import org.apache.commons.lang3.SerializationUtils
import zio.json.DeriveJsonCodec
import zio.json.JsonCodec

/*

Expand Down Expand Up @@ -90,6 +92,8 @@ object BadRequestException {
BadRequestException(s"Invalid value for query parameter '$key'")
def missingQueryParamValue(key: String): BadRequestException =
BadRequestException(s"Missing query parameter '$key'")

implicit val codec: JsonCodec[BadRequestException] = DeriveJsonCodec.gen[BadRequestException]
}

/**
Expand All @@ -99,21 +103,31 @@ 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.
*
* @param message a description of the error.
*/
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.
*
* @param message a description of the error.
*/
case class NotFoundException(message: String) extends RequestRejectedException(message)
object NotFoundException {
val notFound = NotFoundException("The requested data was not found")
val notFound: NotFoundException = NotFoundException("The requested data was not found")

implicit val codec: JsonCodec[NotFoundException] = DeriveJsonCodec.gen[NotFoundException]
}

/**
Expand All @@ -124,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.
Expand Down Expand Up @@ -183,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.
Expand Down
10 changes: 6 additions & 4 deletions webapi/src/main/scala/dsp/valueobjects/Iri.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package dsp.valueobjects
import com.google.gwt.safehtml.shared.UriUtils.encodeAllowEscapes
import org.apache.commons.lang3.StringUtils
import org.apache.commons.validator.routines.UrlValidator
import zio.json.JsonCodec
import zio.json.JsonDecoder
import zio.json.JsonEncoder
import zio.prelude.Validation
Expand Down Expand Up @@ -233,10 +234,11 @@ object Iri {
*/
sealed abstract case class ProjectIri private (value: String) extends Iri
object ProjectIri { self =>
implicit val decoder: JsonDecoder[ProjectIri] =
JsonDecoder[String].mapOrFail(value => ProjectIri.make(value).toEitherWith(e => e.head.getMessage))
implicit val encoder: JsonEncoder[ProjectIri] =
JsonEncoder[String].contramap((projectIri: ProjectIri) => projectIri.value)

implicit val codec: JsonCodec[ProjectIri] = new JsonCodec[ProjectIri](
JsonEncoder[String].contramap(_.value),
JsonDecoder[String].mapOrFail(ProjectIri.make(_).toEitherWith(e => e.head.getMessage))
)

def make(value: String): Validation[ValidationException, ProjectIri] =
if (value.isEmpty) Validation.fail(ValidationException(IriErrorMessages.ProjectIriMissing))
Expand Down
4 changes: 4 additions & 0 deletions webapi/src/main/scala/dsp/valueobjects/Project.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down
Loading
Loading