Skip to content

Commit

Permalink
Add organization deletion route - delete scoped partitions and global
Browse files Browse the repository at this point in the history
  • Loading branch information
dantb committed Sep 27, 2023
1 parent 80f591b commit b90801e
Show file tree
Hide file tree
Showing 13 changed files with 312 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package ch.epfl.bluebrain.nexus.delta.routes

import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directive1
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.{Directive1, Route}
import akka.http.scaladsl.server.Route
import cats.implicits._
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder
Expand All @@ -11,18 +12,22 @@ import ch.epfl.bluebrain.nexus.delta.routes.OrganizationsRoutes.OrganizationInpu
import ch.epfl.bluebrain.nexus.delta.sdk.OrganizationResource
import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclCheck
import ch.epfl.bluebrain.nexus.delta.sdk.circe.CirceUnmarshalling
import ch.epfl.bluebrain.nexus.delta.sdk.directives.AuthDirectives
import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaDirectives._
import ch.epfl.bluebrain.nexus.delta.sdk.directives.{AuthDirectives, DeltaSchemeDirectives}
import ch.epfl.bluebrain.nexus.delta.sdk.directives.DeltaSchemeDirectives
import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities
import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller
import ch.epfl.bluebrain.nexus.delta.sdk.implicits._
import ch.epfl.bluebrain.nexus.delta.sdk.model.BaseUri
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.PaginationConfig
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchParams.OrganizationSearchParams
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults._
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.{PaginationConfig, SearchResults}
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.OrganizationDeleter
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.Organizations
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.Organization
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationRejection
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationRejection._
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.{Organization, OrganizationRejection}
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions._
import io.circe.Decoder
import io.circe.generic.extras.Configuration
Expand All @@ -47,6 +52,7 @@ import scala.annotation.nowarn
final class OrganizationsRoutes(
identities: Identities,
organizations: Organizations,
orgDeleter: OrganizationDeleter,
aclCheck: AclCheck,
schemeDirectives: DeltaSchemeDirectives
)(implicit
Expand Down Expand Up @@ -115,8 +121,17 @@ final class OrganizationsRoutes(
},
// Deprecate organization
delete {
authorizeFor(id, orgs.write).apply {
parameter("rev".as[Int]) { rev => emit(organizations.deprecate(id, rev).mapValue(_.metadata)) }
parameter("rev".as[Int].?, "prune".?(false)) {
case (_, true) =>
authorizeFor(id, orgs.delete).apply {
emit(orgDeleter.delete(id).leftWiden[OrganizationRejection])
}
case (Some(rev), false) =>
authorizeFor(id, orgs.write).apply {
emit(organizations.deprecate(id, rev).mapValue(_.metadata))
}
case (None, false) =>
complete(StatusCodes.BadRequest)
}
}
)
Expand Down Expand Up @@ -154,6 +169,7 @@ object OrganizationsRoutes {
def apply(
identities: Identities,
organizations: Organizations,
orgDeleter: OrganizationDeleter,
aclCheck: AclCheck,
schemeDirectives: DeltaSchemeDirectives
)(implicit
Expand All @@ -163,6 +179,6 @@ object OrganizationsRoutes {
cr: RemoteContextResolution,
ordering: JsonKeyOrdering
): Route =
new OrganizationsRoutes(identities, organizations, aclCheck, schemeDirectives).routes
new OrganizationsRoutes(identities, organizations, orgDeleter, aclCheck, schemeDirectives).routes

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import ch.epfl.bluebrain.nexus.delta.sourcing.Transactors
import izumi.distage.model.definition.{Id, ModuleDef}
import monix.bio.UIO
import monix.execution.Scheduler
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.OrganizationDeleter

/**
* Organizations module wiring config.
Expand All @@ -43,18 +44,23 @@ object OrganizationsModule extends ModuleDef {
)(clock, uuidF)
}

make[OrganizationDeleter].from { (xas: Transactors) =>
OrganizationDeleter(xas)
}

make[OrganizationsRoutes].from {
(
identities: Identities,
organizations: Organizations,
orgDeleter: OrganizationDeleter,
cfg: AppConfig,
aclCheck: AclCheck,
schemeDirectives: DeltaSchemeDirectives,
s: Scheduler,
cr: RemoteContextResolution @Id("aggregate"),
ordering: JsonKeyOrdering
) =>
new OrganizationsRoutes(identities, organizations, aclCheck, schemeDirectives)(
new OrganizationsRoutes(identities, organizations, orgDeleter, aclCheck, schemeDirectives)(
cfg.http.baseUri,
cfg.organizations.pagination,
s,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package ch.epfl.bluebrain.nexus.delta.routes
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.model.headers.OAuth2BearerToken
import akka.http.scaladsl.server.Route
import ch.epfl.bluebrain.nexus.delta.kernel.utils.{UUIDF, UrlUtils}
import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF
import ch.epfl.bluebrain.nexus.delta.kernel.utils.UrlUtils
import ch.epfl.bluebrain.nexus.delta.rdf.Vocabulary.contexts
import ch.epfl.bluebrain.nexus.delta.sdk.acls.AclSimpleCheck
import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.AclAddress
Expand All @@ -12,14 +13,22 @@ import ch.epfl.bluebrain.nexus.delta.sdk.generators.OrganizationGen
import ch.epfl.bluebrain.nexus.delta.sdk.identities.IdentitiesDummy
import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.Caller
import ch.epfl.bluebrain.nexus.delta.sdk.implicits._
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.{OrganizationsConfig, OrganizationsImpl}
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.{events, orgs => orgsPermissions}
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.OrganizationsConfig
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.OrganizationDeleter
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.OrganizationsImpl
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationRejection.OrganizationNonEmpty
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.events
import ch.epfl.bluebrain.nexus.delta.sdk.permissions.Permissions.{orgs => orgsPermissions}
import ch.epfl.bluebrain.nexus.delta.sdk.projects.OwnerPermissionsScopeInitialization
import ch.epfl.bluebrain.nexus.delta.sdk.utils.BaseRouteSpec
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.{Anonymous, Authenticated, Group, Subject}
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Anonymous
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Authenticated
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Group
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label
import ch.epfl.bluebrain.nexus.testkit.bio.IOFromMap
import io.circe.Json
import monix.bio.IO

import java.util.UUID

Expand All @@ -38,7 +47,11 @@ class OrganizationsRoutesSpec extends BaseRouteSpec with IOFromMap {
aclChecker.append,
Set(orgsPermissions.write, orgsPermissions.read)
)
private lazy val orgs = OrganizationsImpl(Set(aopd), config, xas)

private lazy val orgs = OrganizationsImpl(Set(aopd), config, xas)
private lazy val orgDeleter: OrganizationDeleter = id =>
if (id == org1.label) IO.raiseError(OrganizationNonEmpty(id))
else IO.unit

private val caller = Caller(alice, Set(alice, Anonymous, Authenticated(realm), Group("group", realm)))

Expand All @@ -48,6 +61,7 @@ class OrganizationsRoutesSpec extends BaseRouteSpec with IOFromMap {
OrganizationsRoutes(
identities,
orgs,
orgDeleter,
aclChecker,
DeltaSchemeDirectives.onlyResolveOrgUuid(ioFromMap(fixedUuid -> org1.label))
)
Expand Down Expand Up @@ -212,6 +226,33 @@ class OrganizationsRoutesSpec extends BaseRouteSpec with IOFromMap {
}
}

"fail to deprecate an organization if the revision is omitted" in {
Delete("/v1/orgs/org2") ~> addCredentials(OAuth2BearerToken("alice")) ~> routes ~> check {
status shouldEqual StatusCodes.BadRequest
}
}

"delete an organization" in {
aclChecker.append(AclAddress.fromOrg(org2.label), caller.subject -> Set(orgsPermissions.delete)).accepted
Delete("/v1/orgs/org2?prune=true") ~> addCredentials(OAuth2BearerToken("alice")) ~> routes ~> check {
status shouldEqual StatusCodes.OK
}
}

"fail when trying to delete a non-empty organization" in {
aclChecker.append(AclAddress.fromOrg(org1.label), caller.subject -> Set(orgsPermissions.delete)).accepted
Delete("/v1/orgs/org1?prune=true") ~> addCredentials(OAuth2BearerToken("alice")) ~> routes ~> check {
status shouldEqual StatusCodes.Conflict
}
}

"fail to delete an organization without organizations/delete permission" in {
aclChecker.subtract(AclAddress.fromOrg(org2.label), caller.subject -> Set(orgsPermissions.delete)).accepted
Delete("/v1/orgs/org2?prune=true") ~> addCredentials(OAuth2BearerToken("alice")) ~> routes ~> check {
status shouldEqual StatusCodes.Forbidden
}
}

"fail fetch an organization without organizations/read permission" in {
aclChecker.delete(Label.unsafe("org1")).accepted
forAll(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import java.time.Instant
/**
* Operations pertaining to managing Access Control Lists.
*/
trait Acls {
trait Acls extends PurgeAcl {

/**
* Fetches the ACL resource for an ''address'' on the current revision.
Expand Down Expand Up @@ -190,6 +190,7 @@ trait Acls {
*/
def subtract(acl: Acl, rev: Int)(implicit caller: Subject): IO[AclRejection, AclResource]

// DTB TODO - is this more of a deprecate than a delete?
/**
* Delete all ''acl'' on the passed ''address''.
*
Expand All @@ -200,17 +201,20 @@ trait Acls {
*/
def delete(address: AclAddress, rev: Int)(implicit caller: Subject): IO[AclRejection, AclResource]

/**
* Hard deletes events and states for the given acl address This is meant to be used internally for the project
* deletion feature
*/
def internalDelete(project: AclAddress): UIO[Unit]

private def filterSelf(resource: AclResource)(implicit caller: Caller): AclResource =
resource.map(_.filter(caller.identities))

}

trait PurgeAcl {

/**
* Hard deletes events and states for the given acl address. This is meant to be used internally for project and
* organization deletion.
*/
def purge(project: AclAddress): UIO[Unit]
}

object Acls {

/**
Expand Down Expand Up @@ -363,7 +367,7 @@ object Acls {
def projectDeletionTask(acls: Acls): ProjectDeletionTask = new ProjectDeletionTask {
override def apply(project: ProjectRef)(implicit subject: Subject): Task[ProjectDeletionReport.Stage] =
acls
.internalDelete(project)
.purge(project)
.as(
ProjectDeletionReport.Stage("acls", "The acl has been deleted.")
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ final class AclsImpl private (

private def eval(cmd: AclCommand): IO[AclRejection, AclResource] = log.evaluate(cmd.address, cmd).map(_._2.toResource)

override def internalDelete(project: AclAddress): UIO[Unit] = log.delete(project)
override def purge(project: AclAddress): UIO[Unit] = log.delete(project)
}

object AclsImpl {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package ch.epfl.bluebrain.nexus.delta.sdk.organizations

import cats.syntax.all._
import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode
import ch.epfl.bluebrain.nexus.delta.sdk.acls.Acls
import ch.epfl.bluebrain.nexus.delta.sdk.acls.model.AclAddress
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationRejection.OrganizationNonEmpty
import ch.epfl.bluebrain.nexus.delta.sourcing.{MD5, Transactors}
import ch.epfl.bluebrain.nexus.delta.sourcing.implicits._
import ch.epfl.bluebrain.nexus.delta.sourcing.model.EntityType
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label
import com.typesafe.scalalogging.Logger
import doobie.{ConnectionIO, Update}
import doobie.implicits._
import doobie.util.update.Update0
import monix.bio.IO
import monix.bio.UIO

trait OrganizationDeleter {
def delete(id: Label): IO[OrganizationNonEmpty, Unit]
}

object OrganizationDeleter {

def apply(xas: Transactors): OrganizationDeleter = new OrganizationDeleter {

private val logger: Logger = Logger[OrganizationDeleter]

def delete(id: Label): IO[OrganizationNonEmpty, Unit] =
for {
orgIsEmpty <- orgIsEmpty(id)
_ <- if (orgIsEmpty) log(s"Deleting empty organization $id") *> deleteAll(id)
else IO.raiseError(OrganizationNonEmpty(id))
} yield ()

private def log(msg: String): UIO[Unit] = UIO.delay(logger.info(msg))

private def deleteAll(id: Label): UIO[Unit] =
(for {
_ <- List("scoped_events", "scoped_states").traverse(dropPartition(id, _)).void
_ <- List("global_events", "global_states").traverse(deleteGlobal(id, _))
} yield ()).transact(xas.write).void.hideErrors

private def dropPartition(id: Label, table: String): ConnectionIO[Unit] =
Update0(s"DROP TABLE IF EXISTS ${table}_${MD5.hash(id.value)}", None).run.void

private def deleteGlobal(id: Label, table: String): ConnectionIO[Unit] =
for {
_ <- deleteGlobalQuery(Organizations.encodeId(id), Organizations.entityType, table)
_ <- deleteGlobalQuery(Acls.encodeId(AclAddress.fromOrg(id)), Acls.entityType, table)
} yield ()

private def deleteGlobalQuery(id: IriOrBNode.Iri, tpe: EntityType, table: String): ConnectionIO[Unit] =
Update[(EntityType, IriOrBNode.Iri)](s"DELETE FROM $table WHERE" ++ " type = ? AND id = ?").run((tpe, id)).void

private def orgIsEmpty(id: Label): UIO[Boolean] =
sql"SELECT type from scoped_events WHERE org = $id LIMIT 1"
.query[Label]
.option
.map(_.isEmpty)
.transact(xas.read)
.hideErrors
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,8 @@ object Organizations {

command match {
case c: CreateOrganization => create(c)
case c: UpdateOrganization => update(c)
case c: DeprecateOrganization => deprecate(c)
case u: UpdateOrganization => update(u)
case d: DeprecateOrganization => deprecate(d)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,24 @@ import cats.effect.Clock
import ch.epfl.bluebrain.nexus.delta.kernel.kamon.KamonMetricComponent
import ch.epfl.bluebrain.nexus.delta.kernel.search.Pagination
import ch.epfl.bluebrain.nexus.delta.kernel.utils.UUIDF
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.{SearchParams, SearchResults}
import ch.epfl.bluebrain.nexus.delta.sdk.OrganizationResource
import ch.epfl.bluebrain.nexus.delta.sdk.ScopeInitialization
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchParams
import ch.epfl.bluebrain.nexus.delta.sdk.model.search.SearchResults
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.Organizations.entityType
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.OrganizationsImpl.OrganizationsLog
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationCommand
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationCommand._
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationEvent
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationRejection
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationRejection._
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.{OrganizationCommand, OrganizationEvent, OrganizationRejection, OrganizationState}
import ch.epfl.bluebrain.nexus.delta.sdk.organizations.model.OrganizationState
import ch.epfl.bluebrain.nexus.delta.sdk.syntax._
import ch.epfl.bluebrain.nexus.delta.sdk.{OrganizationResource, ScopeInitialization}
import ch.epfl.bluebrain.nexus.delta.sourcing._
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity.Subject
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Label
import monix.bio.{IO, UIO}
import monix.bio.IO
import monix.bio.UIO

final class OrganizationsImpl private (
log: OrganizationsLog,
Expand Down
Loading

0 comments on commit b90801e

Please sign in to comment.