Skip to content

Commit

Permalink
feat: Add endpoint to erase projects (DEV-3681) (#3272)
Browse files Browse the repository at this point in the history
  • Loading branch information
seakayone authored Jun 14, 2024
1 parent affee1f commit 488788a
Show file tree
Hide file tree
Showing 41 changed files with 608 additions and 96 deletions.
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ services:
- JWT_ISSUER=0.0.0.0:3333
- JWT_SECRET=UP 4888, nice 4-8-4 steam engine
- SIPI_USE_LOCAL_DEV=false
- ALLOW_ERASE_PROJECTS=true
# - JWT_DISABLE_AUTH=true # Uncomment this line if you want to disable authentication for the ingest service
deploy:
resources:
Expand Down Expand Up @@ -115,6 +116,7 @@ services:
- KNORA_WEBAPI_DSP_INGEST_AUDIENCE=http://localhost:3340
- KNORA_WEBAPI_DSP_INGEST_BASE_URL=http://ingest:3340
- DSP_API_LOG_LEVEL=INFO
- ALLOW_ERASE_PROJECTS=true
# - DSP_API_LOG_APPENDER=JSON # if this variable is set, JSON logs are activated locally
deploy:
resources:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ class UserRestServiceSpec extends CoreSpec with ImplicitSender {
)
assertFailsWithA[BadRequestException](
exit,
"User http://rdfh.ch/users/normaluser is not a member of project http://rdfh.ch/projects/00FF. A user needs to be a member of the project to be added as project admin.",
"User http://rdfh.ch/users/normaluser is not member of project http://rdfh.ch/projects/00FF.",
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import org.knora.webapi.config.Fuseki
import org.knora.webapi.config.Triplestore
import org.knora.webapi.slice.admin.api.model.MaintenanceRequests.AssetId
import org.knora.webapi.slice.admin.domain.model.KnoraProject
import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode
import org.knora.webapi.testcontainers.FusekiTestContainer

object ProjectImportServiceIT extends ZIOSpecDefault {
Expand Down Expand Up @@ -132,6 +133,8 @@ final case class DspIngestClientITMock() extends DspIngestClient {
originalMimeType = Some("text/plain"),
),
)

override def eraseProject(shortcode: Shortcode): Task[Unit] = ZIO.unit
}
object DspIngestClientITMock {
val layer = ZLayer.derive[DspIngestClientITMock]
Expand Down
4 changes: 4 additions & 0 deletions webapi/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -453,4 +453,8 @@ app {
interval = 5 seconds
}

features {
allow-erase-projects = false
allow-erase-projects = ${?ALLOW_ERASE_PROJECTS}
}
}
10 changes: 8 additions & 2 deletions webapi/src/main/scala/org/knora/webapi/config/AppConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ final case class AppConfig(
instrumentationServerConfig: InstrumentationServerConfig,
jwt: JwtConfig,
dspIngest: DspIngestConfig,
features: Features,
) {
val tmpDataDirPath: zio.nio.file.Path = zio.nio.file.Path(this.tmpDatadir)
}
Expand Down Expand Up @@ -173,16 +174,20 @@ final case class InstrumentationServerConfig(
interval: Duration,
)

final case class Features(allowEraseProjects: Boolean)

object AppConfig {
type AppConfigurationsTest = AppConfig & DspIngestConfig & Triplestore & Sipi
type AppConfigurationsTest = AppConfig & DspIngestConfig & Triplestore & Features & Sipi
type AppConfigurations = AppConfigurationsTest & InstrumentationServerConfig & JwtConfig & KnoraApi

val descriptor: Config[AppConfig] = deriveConfig[AppConfig].mapKey(toKebabCase)

val layer: ULayer[AppConfigurations] = {
val appConfigLayer = ZLayer {
val source = TypesafeConfigProvider.fromTypesafeConfig(ConfigFactory.load().getConfig("app").resolve)
read(descriptor from source).orDie
read(descriptor from source)
.tap(c => ZIO.logInfo("Feature: ALLOW_ERASE_PROJECTS enabled").when(c.features.allowEraseProjects))
.orDie
}
projectAppConfigurations(appConfigLayer).tap(_ => ZIO.logInfo(">>> AppConfig Initialized <<<"))
}
Expand All @@ -194,6 +199,7 @@ object AppConfig {
appConfigLayer.project(_.dspIngest) ++
appConfigLayer.project(_.triplestore) ++
appConfigLayer.project(_.instrumentationServerConfig) ++
appConfigLayer.project(_.features) ++
appConfigLayer.project { appConfig =>
val jwtConfig = appConfig.jwt
val issuerFromConfigOrDefault: Option[String] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import zio.URLayer
import org.knora.webapi.config.AppConfig
import org.knora.webapi.responders.IriService
import org.knora.webapi.slice.admin.domain.AdminDomainModule
import org.knora.webapi.slice.admin.domain.service.DspIngestClient
import org.knora.webapi.slice.admin.repo.AdminRepoModule
import org.knora.webapi.slice.common.repo.service.PredicateObjectMapper
import org.knora.webapi.slice.infrastructure.CacheManager
import org.knora.webapi.slice.ontology.domain.service.OntologyRepo
import org.knora.webapi.slice.ontology.repo.service.OntologyCache
import org.knora.webapi.slice.resourceinfo.domain.IriConverter
import org.knora.webapi.store.triplestore.api.TriplestoreService

object AdminModule {
Expand All @@ -22,8 +25,11 @@ object AdminModule {
// format: off
AppConfig &
CacheManager &
DspIngestClient &
IriService &
IriConverter &
OntologyRepo &
OntologyCache &
PredicateObjectMapper &
TriplestoreService
// format: on
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package org.knora.webapi.slice.admin.api
import zio.ZLayer

import org.knora.webapi.config.AppConfig
import org.knora.webapi.config.Features
import org.knora.webapi.responders.admin.AssetPermissionsResponder
import org.knora.webapi.responders.admin.ListsResponder
import org.knora.webapi.responders.admin.PermissionsResponder
Expand All @@ -24,6 +25,7 @@ import org.knora.webapi.slice.admin.domain.service.KnoraProjectService
import org.knora.webapi.slice.admin.domain.service.KnoraUserService
import org.knora.webapi.slice.admin.domain.service.KnoraUserToUserConverter
import org.knora.webapi.slice.admin.domain.service.PasswordService
import org.knora.webapi.slice.admin.domain.service.ProjectEraseService
import org.knora.webapi.slice.admin.domain.service.ProjectExportService
import org.knora.webapi.slice.admin.domain.service.ProjectImportService
import org.knora.webapi.slice.admin.domain.service.ProjectService
Expand All @@ -44,6 +46,7 @@ object AdminApiModule {
AuthorizationRestService &
BaseEndpoints &
CacheManager &
Features &
GroupService &
HandlerMapper &
KnoraGroupService &
Expand All @@ -56,6 +59,7 @@ object AdminApiModule {
OntologyCache &
PasswordService &
PermissionsResponder &
ProjectEraseService &
ProjectExportService &
ProjectImportService &
ProjectService &
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,16 @@ final case class ProjectsEndpoints(
.out(jsonBody[ProjectOperationResponseADM])
.description("Deletes a project identified by the IRI.")

val deleteAdminProjectsByProjectShortcodeErase = baseEndpoints.securedEndpoint.delete
.in(projectsByShortcode / "erase")
.out(jsonBody[ProjectOperationResponseADM])
.description(
"""|!ATTENTION! Erase a project with the given shortcode.
|This will permanently and irrecoverably remove the project and all of its assets.
|Authorization: Requires system admin permissions.
|Only available if the feature has been configured on the server side.""".stripMargin,
)

val getAdminProjectsExports = baseEndpoints.securedEndpoint.get
.in(projectsBase / `export`)
.out(jsonBody[Chunk[ProjectExportInfoResponse]])
Expand Down Expand Up @@ -209,6 +219,7 @@ final case class ProjectsEndpoints(
Public.getAdminProjectsKeywordsByProjectIri,
) ++ Seq(
Secured.deleteAdminProjectsByIri,
Secured.deleteAdminProjectsByProjectShortcodeErase,
Secured.getAdminProjectsByIriAllData,
Secured.getAdminProjectsByProjectIriAdminMembers,
Secured.getAdminProjectsByProjectIriMembers,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ final case class ProjectsEndpointsHandler(
user => (id: ProjectIri) => restService.deleteProject(id, user),
)

val deleteAdminProjectsByProjectShortcodeEraseHandler =
SecuredEndpointHandler(
projectsEndpoints.Secured.deleteAdminProjectsByProjectShortcodeErase,
user => (id: Shortcode) => restService.eraseProject(id, user),
)

val getAdminProjectsExportsHandler =
SecuredEndpointHandler(
projectsEndpoints.Secured.getAdminProjectsExports,
Expand Down Expand Up @@ -220,6 +226,7 @@ final case class ProjectsEndpointsHandler(
getAdminProjectsByProjectShortcodeAdminMembersHandler,
getAdminProjectsByProjectShortnameAdminMembersHandler,
deleteAdminProjectsByIriHandler,
deleteAdminProjectsByProjectShortcodeEraseHandler,
getAdminProjectsExportsHandler,
postAdminProjectsByShortcodeExportHandler,
postAdminProjectsByShortcodeExportAwaitingHandler,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ package org.knora.webapi.slice.admin.api.service
import zio.*

import dsp.errors.BadRequestException
import dsp.errors.ForbiddenException
import dsp.errors.NotFoundException
import org.knora.webapi.config.Features
import org.knora.webapi.responders.admin.PermissionsResponder
import org.knora.webapi.slice.admin.api.model.*
import org.knora.webapi.slice.admin.api.model.ProjectDataGetResponseADM
Expand All @@ -26,6 +28,7 @@ import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortname
import org.knora.webapi.slice.admin.domain.model.KnoraProject.Status
import org.knora.webapi.slice.admin.domain.model.User
import org.knora.webapi.slice.admin.domain.service.KnoraProjectService
import org.knora.webapi.slice.admin.domain.service.ProjectEraseService
import org.knora.webapi.slice.admin.domain.service.ProjectExportService
import org.knora.webapi.slice.admin.domain.service.ProjectImportService
import org.knora.webapi.slice.admin.domain.service.ProjectService
Expand All @@ -39,10 +42,12 @@ final case class ProjectRestService(
projectService: ProjectService,
knoraProjectService: KnoraProjectService,
permissionResponder: PermissionsResponder,
projectEraseService: ProjectEraseService,
projectExportService: ProjectExportService,
projectImportService: ProjectImportService,
userService: UserService,
auth: AuthorizationRestService,
features: Features,
) {

/**
Expand Down Expand Up @@ -124,6 +129,23 @@ final case class ProjectRestService(
external <- format.toExternal(internal)
} yield external

def eraseProject(shortcode: Shortcode, user: User): Task[ProjectOperationResponseADM] =
for {
_ <- auth.ensureSystemAdmin(user)
_ <- ZIO.unless(features.allowEraseProjects)(
ZIO.fail(ForbiddenException("The feature to erase projects is not enabled.")),
)
internal <- projectService
.findByShortcode(shortcode)
.someOrFail(NotFoundException(s"$shortcode not found"))
project <- knoraProjectService
.findByShortcode(shortcode)
.someOrFail(NotFoundException(s"$shortcode not found"))
_ <- ZIO.logInfo(s"${user.userIri} erases project $shortcode")
_ <- projectEraseService.eraseProject(project)
external <- format.toExternal(ProjectOperationResponseADM(internal))
} yield external

/**
* Updates a project, identified by its [[ProjectIri]].
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import org.knora.webapi.slice.admin.domain.service.maintenance.MaintenanceServic
import org.knora.webapi.slice.admin.repo.AdminRepoModule
import org.knora.webapi.slice.infrastructure.CacheManager
import org.knora.webapi.slice.ontology.domain.service.OntologyRepo
import org.knora.webapi.slice.ontology.repo.service.OntologyCache
import org.knora.webapi.slice.resourceinfo.domain.IriConverter
import org.knora.webapi.store.triplestore.api.TriplestoreService

object AdminDomainModule {
Expand All @@ -28,8 +30,11 @@ object AdminDomainModule {
AdminRepoModule.Provided &
AppConfig &
CacheManager &
DspIngestClient &
IriService &
IriConverter &
OntologyRepo &
OntologyCache &
TriplestoreService
// format: on

Expand All @@ -44,6 +49,7 @@ object AdminDomainModule {
MaintenanceService &
PasswordService &
ProjectService &
ProjectEraseService &
UserService
// format: on

Expand All @@ -56,6 +62,7 @@ object AdminDomainModule {
KnoraUserToUserConverter.layer,
MaintenanceService.layer,
PasswordService.layer,
ProjectEraseService.layer,
ProjectService.layer,
UserService.layer,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import zio.Task

import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri
import org.knora.webapi.slice.admin.repo.service.EntityWithId
import org.knora.webapi.slice.common.repo.service.Repository
import org.knora.webapi.slice.common.repo.service.CrudRepository
import org.knora.webapi.slice.resourceinfo.domain.InternalIri

final case class AdministrativePermission(
Expand Down Expand Up @@ -53,9 +53,11 @@ object AdministrativePermissionPart {
)
}

trait AdministrativePermissionRepo extends Repository[AdministrativePermission, PermissionIri] {
trait AdministrativePermissionRepo extends CrudRepository[AdministrativePermission, PermissionIri] {

def findByGroupAndProject(groupIri: GroupIri, projectIri: ProjectIri): Task[Option[AdministrativePermission]]

def save(permission: AdministrativePermission): Task[AdministrativePermission]
def findByProject(projectIri: ProjectIri): Task[Chunk[AdministrativePermission]]

final def findByProject(project: KnoraProject): Task[Chunk[AdministrativePermission]] = findByProject(project.id)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright © 2021 - 2024 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors.
* SPDX-License-Identifier: Apache-2.0
*/

package org.knora.webapi.slice.admin.domain.model

import zio.Chunk
import zio.Task

import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri
import org.knora.webapi.slice.admin.repo.service.EntityWithId
import org.knora.webapi.slice.common.repo.service.CrudRepository

final case class DefaultObjectAccessPermission(
id: PermissionIri,
forProject: ProjectIri,
) extends EntityWithId[PermissionIri]

trait DefaultObjectAccessPermissionRepo extends CrudRepository[DefaultObjectAccessPermission, PermissionIri] {

def findByProject(projectIri: ProjectIri): Task[Chunk[DefaultObjectAccessPermission]]

final def findByProject(project: KnoraProject): Task[Chunk[DefaultObjectAccessPermission]] = findByProject(project.id)
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ trait DspIngestClient {

def importProject(shortcode: Shortcode, fileToImport: Path): Task[Path]

def eraseProject(shortcode: Shortcode): Task[Unit]

def getAssetInfo(shortcode: Shortcode, assetId: AssetId): Task[AssetInfoResponse]
}

Expand Down Expand Up @@ -100,6 +102,12 @@ final case class DspIngestClientLive(
_ <- ZIO.logInfo(s"Response from ingest :${response.code}")
} yield exportFile

override def eraseProject(shortcode: Shortcode): Task[Unit] = for {
request <- authenticatedRequest.map(_.delete(uri"${projectsPath(shortcode)}/erase"))
response <- ZIO.blocking(request.send(backend = sttpBackend))
_ <- ZIO.logInfo(s"Response from ingest :${response.body}")
} yield ()

override def importProject(shortcode: Shortcode, fileToImport: Path): Task[Path] = ZIO.scoped {
for {
importUrl <- ZIO.fromEither(URL.decode(s"${projectsPath(shortcode)}/import"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,16 @@ import org.knora.webapi.slice.admin.domain.model.GroupName
import org.knora.webapi.slice.admin.domain.model.GroupSelfJoin
import org.knora.webapi.slice.admin.domain.model.GroupStatus
import org.knora.webapi.slice.admin.domain.model.KnoraGroup
import org.knora.webapi.slice.common.repo.service.Repository
import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri
import org.knora.webapi.slice.common.repo.service.CrudRepository

trait KnoraGroupRepo extends Repository[KnoraGroup, GroupIri] {
trait KnoraGroupRepo extends CrudRepository[KnoraGroup, GroupIri] {

def findByName(name: GroupName): Task[Option[KnoraGroup]]

def existsByName(name: GroupName): Task[Boolean] = findByName(name).map(_.isDefined)

/**
* Saves the user group, returns the created data. Updates not supported.
*
* @param group The [[KnoraGroup]] to be saved, can be an update or a creation.
* @return The saved entity.
*/
def save(group: KnoraGroup): Task[KnoraGroup]
def findByProjectIri(projectIri: ProjectIri): Task[Chunk[KnoraGroup]]
}

object KnoraGroupRepo {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ case class KnoraGroupService(

def findByIds(ids: Seq[GroupIri]): Task[Chunk[KnoraGroup]] = knoraGroupRepo.findByIds(ids)

def findByProject(project: KnoraProject): Task[Chunk[KnoraGroup]] = knoraGroupRepo.findByProjectIri(project.id)

def deleteAll(groups: Seq[KnoraGroup]): Task[Unit] =
knoraGroupRepo.deleteAll(groups)

def createGroup(request: GroupCreateRequest, project: KnoraProject): Task[KnoraGroup] =
for {
_ <- ensureGroupNameIsUnique(request.name)
Expand Down
Loading

0 comments on commit 488788a

Please sign in to comment.