From c23baca47a59a6deced960e4e708ee9c9e550749 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 3 Jun 2024 14:45:28 +0200 Subject: [PATCH] introduce-booking-project-and-nested-booking-items (#57) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/57 Reviewed-by: Marc Sandlus --- ...ects-booking-items-and-hosting-entities.md | 288 ++++++++++++++++ .../booking/item/HsBookingItemController.java | 6 +- .../hs/booking/item/HsBookingItemEntity.java | 75 ++-- .../booking/item/HsBookingItemRepository.java | 2 +- .../project/HsBookingProjectController.java | 114 ++++++ .../project/HsBookingProjectEntity.java | 113 ++++++ .../HsBookingProjectEntityPatcher.java | 22 ++ .../project/HsBookingProjectRepository.java | 21 ++ .../asset/HsHostingAssetController.java | 16 +- .../hosting/asset/HsHostingAssetEntity.java | 43 +-- .../asset/HsHostingAssetRepository.java | 8 +- ...sManagedWebspaceHostingAssetValidator.java | 2 +- .../rbac/rbacdef/InsertTriggerGenerator.java | 6 +- .../hs-booking/api-mappings.yaml | 2 + .../hs-booking/hs-booking-item-schemas.yaml | 4 +- .../hs-booking/hs-booking-items.yaml | 10 +- .../hs-booking-project-schemas.yaml | 40 +++ .../hs-booking-projects-with-uuid.yaml | 83 +++++ .../hs-booking/hs-booking-projects.yaml | 58 ++++ .../api-definition/hs-booking/hs-booking.yaml | 9 + .../6100-hs-booking-project.sql | 22 ++ .../6103-hs-booking-project-rbac.md | 63 ++++ .../6103-hs-booking-project-rbac.sql} | 100 +++--- .../6108-hs-booking-project-test-data.sql} | 24 +- .../6200-hs-booking-item.sql} | 8 +- .../6203-hs-booking-item-rbac.md} | 31 +- .../6203-hs-booking-item-rbac.sql | 277 +++++++++++++++ .../6208-hs-booking-item-test-data.sql | 58 ++++ .../7010-hs-hosting-asset.sql | 7 +- ...7013-hs-hosting-asset-rbac-CLOUD_SERVER.md | 22 +- ...13-hs-hosting-asset-rbac-MANAGED_SERVER.md | 22 +- ...-hs-hosting-asset-rbac-MANAGED_WEBSPACE.md | 22 +- .../7013-hs-hosting-asset-rbac.md | 41 +-- .../7013-hs-hosting-asset-rbac.sql | 93 +++-- .../7018-hs-hosting-asset-test-data.sql | 51 +-- .../db/changelog/db.changelog-master.yaml | 12 +- .../hsadminng/arch/ArchitectureTest.java | 5 +- ...HsBookingItemControllerAcceptanceTest.java | 108 +++--- .../HsBookingItemEntityPatcherUnitTest.java | 4 +- .../item/HsBookingItemEntityUnitTest.java | 8 +- ...sBookingItemRepositoryIntegrationTest.java | 80 +++-- .../hs/booking/item/TestHsBookingItem.java | 4 +- ...ookingProjectControllerAcceptanceTest.java | 289 ++++++++++++++++ ...HsBookingProjectEntityPatcherUnitTest.java | 74 ++++ .../HsBookingProjectEntityUnitTest.java | 27 ++ ...okingProjectRepositoryIntegrationTest.java | 326 ++++++++++++++++++ .../booking/project/TestHsBookingProject.java | 15 + ...sHostingAssetControllerAcceptanceTest.java | 115 +++--- .../asset/HsHostingAssetEntityUnitTest.java | 2 +- ...HostingAssetRepositoryIntegrationTest.java | 91 ++--- ...WebspaceHostingAssetValidatorUnitTest.java | 5 +- ...fficeDebitorRepositoryIntegrationTest.java | 2 +- .../office/debitor/TestHsOfficeDebitor.java | 1 + .../hs/office/migration/ImportOfficeData.java | 1 + 54 files changed, 2437 insertions(+), 495 deletions(-) create mode 100644 doc/projects-booking-items-and-hosting-entities.md create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectController.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntity.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntityPatcher.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepository.java create mode 100644 src/main/resources/api-definition/hs-booking/hs-booking-project-schemas.yaml create mode 100644 src/main/resources/api-definition/hs-booking/hs-booking-projects-with-uuid.yaml create mode 100644 src/main/resources/api-definition/hs-booking/hs-booking-projects.yaml create mode 100644 src/main/resources/db/changelog/6-hs-booking/610-booking-project/6100-hs-booking-project.sql create mode 100644 src/main/resources/db/changelog/6-hs-booking/610-booking-project/6103-hs-booking-project-rbac.md rename src/main/resources/db/changelog/6-hs-booking/{601-booking-item/6013-hs-booking-item-rbac.sql => 610-booking-project/6103-hs-booking-project-rbac.sql} (59%) rename src/main/resources/db/changelog/6-hs-booking/{601-booking-item/6018-hs-booking-item-test-data.sql => 610-booking-project/6108-hs-booking-project-test-data.sql} (51%) rename src/main/resources/db/changelog/6-hs-booking/{601-booking-item/6010-hs-booking-item.sql => 620-booking-item/6200-hs-booking-item.sql} (75%) rename src/main/resources/db/changelog/6-hs-booking/{601-booking-item/6013-hs-booking-item-rbac.md => 620-booking-item/6203-hs-booking-item-rbac.md} (63%) create mode 100644 src/main/resources/db/changelog/6-hs-booking/620-booking-item/6203-hs-booking-item-rbac.sql create mode 100644 src/main/resources/db/changelog/6-hs-booking/620-booking-item/6208-hs-booking-item-test-data.sql create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectControllerAcceptanceTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntityPatcherUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntityUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/booking/project/TestHsBookingProject.java diff --git a/doc/projects-booking-items-and-hosting-entities.md b/doc/projects-booking-items-and-hosting-entities.md new file mode 100644 index 00000000..e2a2ba83 --- /dev/null +++ b/doc/projects-booking-items-and-hosting-entities.md @@ -0,0 +1,288 @@ +## HSAdmin-NG +### Project/BookingItems/HostingEntities + +__ATTENTION__: The notation uses UML clas diagram elements, but partly with different meanings. See Agenda. + +```mermaid +classDiagram + direction TD + + Partner o-- "0..n" Membership + Partner *-- "1..n" Debitor + Debitor *-- "1..n" Project + + Project o-- "0..n" PrivateCloudBI + Project o-- "0..n" CloudServerBI + Project o-- "0..n" ManagedServerBI + Project o-- "0..n" ManagedWebspaceBI + + PrivateCloudBI o-- "0..n" ManagedServerBI + PrivateCloudBI o-- "0..n" CloudServerBI + + CloudServerBI *-- CloudServerHE + + ManagedServerBI *-- ManagedServerHE + ManagedServerBI o-- "0..n" ManagedWebspaceBI + ManagedWebspaceBI *-- ManagedWebspaceHE + + ManagedWebspaceHE *-- "1..n" UnixUserHE + ManagedWebspaceHE o-- "0..n" DomainDNSSetupHE + ManagedWebspaceHE o-- "0..n" DomainHttpSetupHE + ManagedWebspaceHE o-- "0..n" DomainEMailSetupHE + ManagedWebspaceHE o-- "0..n" EMailAliasHE + DomainEMailSetupHE o-- "0..n" EMailAddressHE + ManagedWebspaceHE o-- "0..n" MariaDBUserHE + MariaDBUserHE o-- "0..n" MariaDBHE + ManagedWebspaceHE o-- "0..n" PostgresDBUserHE + PostgresDBUserHE o-- "0..n" PostgresDBHE + + DomainHttpSetupHE --|> UnixUserHE : assignedToAsset + + ManagedWebspaceHE --|> ManagedServerHE + + namespace Office { + class Partner { + } + + class Membership { + } + + class Debitor { + + } + } + + namespace Booking { + class Project { + +caption + +create() + } + class PrivateCloudBI { + +caption + ~resources = [ + ⠀⠀+CPUs + ⠀⠀+RAM + ⠀⠀+SSD + ⠀⠀+HDD + ⠀⠀+Traffic + ] + + +book() + } + class CloudServerBI { + +caption + ~resources = [ + ⠀⠀+CPUs + ⠀⠀+RAM + ⠀⠀+SSD + ⠀⠀+HDD + ⠀⠀+Traffic + ] + + +book() + } + class ManagedServerBI { + +caption + ~respources = [ + ⠀⠀+CPUs + ⠀⠀+RAM + ⠀⠀+SSD + ⠀⠀+HDD + ⠀⠀+Traffic + ] + + +book() + } + class ManagedWebspaceBI { + +caption + ~resources = [ + ⠀⠀+SSD + ⠀⠀+HDD + ⠀⠀+Traffic + ⠀⠀+MultiOptions + ⠀⠀+Daemons + ] + + +book() + } + } + + style Project stroke:blue,stroke-width:4px + style PrivateCloudBI stroke:blue,stroke-width:4px + style CloudServerBI stroke:blue,stroke-width:4px + style ManagedServerBI stroke:blue,stroke-width:4px + style ManagedWebspaceBI stroke:blue,stroke-width:4px + + %% --------------------------------------------------------- + + namespace HostingServers { + %% separate (pseudo-) namespace just for better rendering + + class CloudServerHE { + -identifier, e.g. "vm1234" + -caption := bi.caption? + -parentAsset := parentHost + -identifier := serverName + -create() + } + class ManagedServerHE { + -identifier, e.g. "vm1234" + -caption := bi.caption? + -parentAsset := parentHost + -identifier := serverName + ~config = [ + ⠀⠀+installed Software + ] + -create() + } + } + + namespace Hosting { + class ManagedWebspaceHE { + -parentAsset := parentManagedServer + -identifier : webspaceName + +caption + + -create() + } + + class UnixUserHE { + +identifier ["xyz00-..."] + +caption + ~config = [ + ⠀⠀+SSD Soft Quota + ⠀⠀+SSD Hard Quota + ⠀⠀+HDD Soft Quota + ⠀⠀+HDD Hard Quota + ⠀⠀#shell + ⠀⠀#password + ] + + +create() + } + class DomainDNSSetupHE { + +identifier, e.g. "example.com" + +caption + + +create() + } + class DomainHttpSetupHE { + +identifier, e.g. "example.com" + +caption + + +create() + } + class DomainEMailSetupHE { + +identifier, e.g. "example.com" + +caption + + +create() + } + class EMailAliasHE { + +identifier, e.g "xyz00-..." + +caption + + ~config = [ + ⠀⠀+target[] + ] + + +create() + } + class EMailAddressHE { + +identifier, e.g. "test@example.org" + +caption + ~config = [ + ⠀⠀+sub-domain + ⠀⠀+local-part + ⠀⠀+target + ] + + +create() + } + class MariaDBUserHE { + +identifier, e.g. "xyz00_mydb" + +caption + config = [ + ⠀⠀#password + ] + + +create() + } + class MariaDBHE { + +identifier, e.g. "xyz00_mydb" + +caption + ~config = [ + ⠀⠀+encoding + ] + + +create() + } + class PostgresDBUserHE { + +identifier, e.g. "xyz00_mydb" + +caption + ~config = [ + ⠀⠀#password + ] + + +create() + } + class PostgresDBHE { + +identifier, e.g. "xyz00_mydb" + +caption + + ~config = [ + ⠀⠀+encoding + ⠀⠀+extensions + ] + +create() + } + } + + style CloudServerHE stroke:orange,stroke-width:4px + style ManagedServerHE stroke:orange,stroke-width:4px + style ManagedWebspaceHE stroke:orange,stroke-width:4px + style UnixUserHE stroke:blue,stroke-width:4px + style DomainDNSSetupHE stroke:blue,stroke-width:4px + style DomainHttpSetupHE stroke:blue,stroke-width:4px + style DomainEMailSetupHE stroke:blue,stroke-width:4px + style EMailAliasHE stroke:blue,stroke-width:4px + style EMailAddressHE stroke:blue,stroke-width:4px + style MariaDBUserHE stroke:blue,stroke-width:4px + style MariaDBHE stroke:blue,stroke-width:4px + style PostgresDBUserHE stroke:blue,stroke-width:4px + style PostgresDBHE stroke:blue,stroke-width:4px + +%% -------------------------------------- + + ParentA o-- ChildA : can contain + ParentB *-- ChildB : contains + + namespace Agenda { + class ParentA { + } + class ChildA { + } + class ParentB { + } + class ChildB { + } + class CreatedByClient { + } + class CreatedAutomatically { + } + class SomeEntity { + ~patchable = [ + %% the following indentations uses two U+2800 to have effect in the rendered diagram + ⠀⠀+first + ⠀⠀+second + ] + -readOnly for client accounts + +readWrite for client accounts + #writeOnly + } + } + + style CreatedByClient stroke:blue,stroke-width:4px + style CreatedAutomatically stroke:orange,stroke-width:4px +end +``` diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java index e3154f76..2ada5e0c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemController.java @@ -34,13 +34,13 @@ public class HsBookingItemController implements HsBookingItemsApi { @Override @Transactional(readOnly = true) - public ResponseEntity> listBookingItemsByDebitorUuid( + public ResponseEntity> listBookingItemsByProjectUuid( final String currentUser, final String assumedRoles, - final UUID debitorUuid) { + final UUID projectUuid) { context.define(currentUser, assumedRoles); - final var entities = bookingItemRepo.findAllByDebitorUuid(debitorUuid); + final var entities = bookingItemRepo.findAllByProjectUuid(projectUuid); final var resources = mapper.mapList(entities, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(resources); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java index 60dd2935..4739c638 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntity.java @@ -9,8 +9,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; -import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity; import net.hostsharing.hsadminng.hs.validation.Validatable; import net.hostsharing.hsadminng.mapper.PatchableMapWrapper; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; @@ -38,14 +37,12 @@ import java.util.Map; import java.util.UUID; import static java.util.Optional.ofNullable; -import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveFromPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.upperInclusiveFromPostgresDateRange; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; -import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; -import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NULLABLE; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT; @@ -55,7 +52,6 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.OWNER; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.TENANT; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn; -import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.fetchedBySql; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @@ -69,7 +65,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify; public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatable { private static Stringify stringify = stringify(HsBookingItemEntity.class) - .withProp(HsBookingItemEntity::getDebitor) + .withProp(HsBookingItemEntity::getProject) .withProp(HsBookingItemEntity::getType) .withProp(e -> e.getValidity().asString()) .withProp(HsBookingItemEntity::getCaption) @@ -83,9 +79,13 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab @Version private int version; - @ManyToOne(optional = false) - @JoinColumn(name = "debitoruuid") - private HsOfficeDebitorEntity debitor; + @ManyToOne + @JoinColumn(name = "projectuuid") + private HsBookingProjectEntity project; + + @ManyToOne + @JoinColumn(name = "parentitemuuid") + private HsBookingItemEntity parentItem; @Column(name = "type") @Enumerated(EnumType.STRING) @@ -139,10 +139,17 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab @Override public String toShortString() { - return ofNullable(debitor).map(HsOfficeDebitorEntity::toShortString).orElse("D-???????") + + return ofNullable(relatedProject()).map(HsBookingProjectEntity::toShortString).orElse("D-???????-?") + ":" + caption; } + private HsBookingProjectEntity relatedProject() { + if (project != null) { + return project; + } + return parentItem == null ? null : parentItem.relatedProject(); + } + @Override public String getPropertiesName() { return "resources"; @@ -155,48 +162,42 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab public static RbacView rbac() { return rbacViewFor("bookingItem", HsBookingItemEntity.class) - .withIdentityView(SQL.query(""" - SELECT bookingItem.uuid as uuid, debitorIV.idName || '-' || cleanIdentifier(bookingItem.caption) as idName - FROM hs_booking_item bookingItem - JOIN hs_office_debitor_iv debitorIV ON debitorIV.uuid = bookingItem.debitorUuid - """)) + .withIdentityView(SQL.projection("caption")) .withRestrictedViewOrderBy(SQL.expression("validity")) .withUpdatableColumns("version", "caption", "validity", "resources") - - .importEntityAlias("debitor", HsOfficeDebitorEntity.class, usingDefaultCase(), - dependsOnColumn("debitorUuid"), - directlyFetchedByDependsOnColumn(), - NOT_NULL) - - .importEntityAlias("debitorRel", HsOfficeRelationEntity.class, usingCase(DEBITOR), - dependsOnColumn("debitorUuid"), - fetchedBySql(""" - SELECT ${columns} - FROM hs_office_relation debitorRel - JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid - WHERE debitor.uuid = ${REF}.debitorUuid - """), - NOT_NULL) - .toRole("debitorRel", ADMIN).grantPermission(INSERT) + .toRole("global", ADMIN).grantPermission(INSERT) // TODO.impl: Why is this necessary to insert test data? .toRole("global", ADMIN).grantPermission(DELETE) + .importEntityAlias("project", HsBookingProjectEntity.class, usingDefaultCase(), + dependsOnColumn("projectUuid"), + directlyFetchedByDependsOnColumn(), + NULLABLE) + .toRole("project", ADMIN).grantPermission(INSERT) + + .importEntityAlias("parentItem", HsBookingItemEntity.class, usingDefaultCase(), + dependsOnColumn("parentItemUuid"), + directlyFetchedByDependsOnColumn(), + NULLABLE) + .toRole("parentItem", ADMIN).grantPermission(INSERT) + .createRole(OWNER, (with) -> { - with.incomingSuperRole("debitorRel", AGENT); + with.incomingSuperRole("project", AGENT); + with.incomingSuperRole("parentItem", AGENT); }) .createSubRole(ADMIN, (with) -> { - with.incomingSuperRole("debitorRel", AGENT); with.permission(UPDATE); }) .createSubRole(AGENT) .createSubRole(TENANT, (with) -> { - with.outgoingSubRole("debitorRel", TENANT); + with.outgoingSubRole("project", TENANT); + with.outgoingSubRole("parentItem", TENANT); with.permission(SELECT); }) - .limitDiagramTo("bookingItem", "debitorRel", "global"); + .limitDiagramTo("bookingItem", "project", "global"); } public static void main(String[] args) throws IOException { - rbac().generateWithBaseFileName("6-hs-booking/601-booking-item/6013-hs-booking-item-rbac"); + rbac().generateWithBaseFileName("6-hs-booking/620-booking-item/6203-hs-booking-item-rbac"); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepository.java index 6d9bd683..cda96233 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepository.java @@ -11,7 +11,7 @@ public interface HsBookingItemRepository extends Repository findAll(); Optional findByUuid(final UUID bookingItemUuid); - List findAllByDebitorUuid(final UUID bookingItemUuid); + List findAllByProjectUuid(final UUID projectItemUuid); HsBookingItemEntity save(HsBookingItemEntity current); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectController.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectController.java new file mode 100644 index 00000000..10230d0b --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectController.java @@ -0,0 +1,114 @@ +package net.hostsharing.hsadminng.hs.booking.project; + +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingProjectsApi; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectInsertResource; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectPatchResource; +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectResource; +import net.hostsharing.hsadminng.mapper.Mapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; + +import java.util.List; +import java.util.UUID; + +@RestController +public class HsBookingProjectController implements HsBookingProjectsApi { + + @Autowired + private Context context; + + @Autowired + private Mapper mapper; + + @Autowired + private HsBookingProjectRepository bookingProjectRepo; + + @Override + @Transactional(readOnly = true) + public ResponseEntity> listBookingProjectsByDebitorUuid( + final String currentUser, + final String assumedRoles, + final UUID debitorUuid) { + context.define(currentUser, assumedRoles); + + final var entities = bookingProjectRepo.findAllByDebitorUuid(debitorUuid); + + final var resources = mapper.mapList(entities, HsBookingProjectResource.class); + return ResponseEntity.ok(resources); + } + + @Override + @Transactional + public ResponseEntity addBookingProject( + final String currentUser, + final String assumedRoles, + final HsBookingProjectInsertResource body) { + + context.define(currentUser, assumedRoles); + + final var entityToSave = mapper.map(body, HsBookingProjectEntity.class); + + final var saved = bookingProjectRepo.save(entityToSave); + + final var uri = + MvcUriComponentsBuilder.fromController(getClass()) + .path("/api/hs/booking/projects/{id}") + .buildAndExpand(saved.getUuid()) + .toUri(); + final var mapped = mapper.map(saved, HsBookingProjectResource.class); + return ResponseEntity.created(uri).body(mapped); + } + + @Override + @Transactional(readOnly = true) + public ResponseEntity getBookingProjectByUuid( + final String currentUser, + final String assumedRoles, + final UUID bookingProjectUuid) { + + context.define(currentUser, assumedRoles); + + final var result = bookingProjectRepo.findByUuid(bookingProjectUuid); + return result + .map(bookingProjectEntity -> ResponseEntity.ok( + mapper.map(bookingProjectEntity, HsBookingProjectResource.class))) + .orElseGet(() -> ResponseEntity.notFound().build()); + } + + @Override + @Transactional + public ResponseEntity deleteBookingIemByUuid( + final String currentUser, + final String assumedRoles, + final UUID bookingProjectUuid) { + context.define(currentUser, assumedRoles); + + final var result = bookingProjectRepo.deleteByUuid(bookingProjectUuid); + return result == 0 + ? ResponseEntity.notFound().build() + : ResponseEntity.noContent().build(); + } + + @Override + @Transactional + public ResponseEntity patchBookingProject( + final String currentUser, + final String assumedRoles, + final UUID bookingProjectUuid, + final HsBookingProjectPatchResource body) { + + context.define(currentUser, assumedRoles); + + final var current = bookingProjectRepo.findByUuid(bookingProjectUuid).orElseThrow(); + + new HsBookingProjectEntityPatcher(current).apply(body); + + final var saved = bookingProjectRepo.save(current); + final var mapped = mapper.map(saved, HsBookingProjectResource.class); + return ResponseEntity.ok(mapped); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntity.java new file mode 100644 index 00000000..aee3242f --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntity.java @@ -0,0 +1,113 @@ +package net.hostsharing.hsadminng.hs.booking.project; + +import lombok.*; +import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; +import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; +import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; +import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; +import net.hostsharing.hsadminng.stringify.Stringify; +import net.hostsharing.hsadminng.stringify.Stringifyable; + +import jakarta.persistence.*; +import java.io.IOException; +import java.util.UUID; + +import static java.util.Optional.ofNullable; +import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.fetchedBySql; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; +import static net.hostsharing.hsadminng.stringify.Stringify.stringify; + +@Builder +@Entity +@Table(name = "hs_booking_project_rv") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class HsBookingProjectEntity implements Stringifyable, RbacObject { + + private static Stringify stringify = stringify(HsBookingProjectEntity.class) + .withProp(HsBookingProjectEntity::getDebitor) + .withProp(HsBookingProjectEntity::getCaption) + .quotedValues(false); + + @Id + @GeneratedValue + private UUID uuid; + + @Version + private int version; + + @ManyToOne(optional = false) + @JoinColumn(name = "debitoruuid") + private HsOfficeDebitorEntity debitor; + + @Column(name = "caption") + private String caption; + + @Override + public String toString() { + return stringify.apply(this); + } + + @Override + public String toShortString() { + return ofNullable(debitor).map(HsOfficeDebitorEntity::toShortString).orElse("D-???????") + + ":" + caption; + } + + public static RbacView rbac() { + return rbacViewFor("project", HsBookingProjectEntity.class) + .withIdentityView(SQL.query(""" + SELECT bookingProject.uuid as uuid, debitorIV.idName || '-' || cleanIdentifier(bookingProject.caption) as idName + FROM hs_booking_project bookingProject + JOIN hs_office_debitor_iv debitorIV ON debitorIV.uuid = bookingProject.debitorUuid + """)) + .withRestrictedViewOrderBy(SQL.expression("caption")) + .withUpdatableColumns("version", "caption") + + .importEntityAlias("debitor", HsOfficeDebitorEntity.class, usingDefaultCase(), + dependsOnColumn("debitorUuid"), + directlyFetchedByDependsOnColumn(), + NOT_NULL) + + .importEntityAlias("debitorRel", HsOfficeRelationEntity.class, usingCase(DEBITOR), + dependsOnColumn("debitorUuid"), + fetchedBySql(""" + SELECT ${columns} + FROM hs_office_relation debitorRel + JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid + WHERE debitor.uuid = ${REF}.debitorUuid + """), + NOT_NULL) + .toRole("debitorRel", ADMIN).grantPermission(INSERT) + .toRole("global", ADMIN).grantPermission(DELETE) + + .createRole(OWNER, (with) -> { + with.incomingSuperRole("debitorRel", AGENT); + }) + .createSubRole(ADMIN, (with) -> { + with.permission(UPDATE); + }) + .createSubRole(AGENT) + .createSubRole(TENANT, (with) -> { + with.outgoingSubRole("debitorRel", TENANT); + with.permission(SELECT); + }) + + .limitDiagramTo("project", "debitorRel", "global"); + } + + public static void main(String[] args) throws IOException { + rbac().generateWithBaseFileName("6-hs-booking/610-booking-project/6103-hs-booking-project-rbac"); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntityPatcher.java new file mode 100644 index 00000000..239fb075 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntityPatcher.java @@ -0,0 +1,22 @@ +package net.hostsharing.hsadminng.hs.booking.project; + +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectPatchResource; +import net.hostsharing.hsadminng.mapper.EntityPatcher; +import net.hostsharing.hsadminng.mapper.OptionalFromJson; + + + +public class HsBookingProjectEntityPatcher implements EntityPatcher { + + private final HsBookingProjectEntity entity; + + public HsBookingProjectEntityPatcher(final HsBookingProjectEntity entity) { + this.entity = entity; + } + + @Override + public void apply(final HsBookingProjectPatchResource resource) { + OptionalFromJson.of(resource.getCaption()) + .ifPresent(entity::setCaption); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepository.java new file mode 100644 index 00000000..b224dad6 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepository.java @@ -0,0 +1,21 @@ +package net.hostsharing.hsadminng.hs.booking.project; + +import org.springframework.data.repository.Repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface HsBookingProjectRepository extends Repository { + + List findAll(); + Optional findByUuid(final UUID bookingProjectUuid); + + List findAllByDebitorUuid(final UUID bookingProjectUuid); + + HsBookingProjectEntity save(HsBookingProjectEntity current); + + int deleteByUuid(final UUID uuid); + + long count(); +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java index 57e91ec5..a645bb78 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java @@ -78,14 +78,14 @@ public class HsHostingAssetController implements HsHostingAssetsApi { public ResponseEntity getAssetByUuid( final String currentUser, final String assumedRoles, - final UUID serverUuid) { + final UUID assetUuid) { context.define(currentUser, assumedRoles); - final var result = assetRepo.findByUuid(serverUuid); + final var result = assetRepo.findByUuid(assetUuid); return result - .map(serverEntity -> ResponseEntity.ok( - mapper.map(serverEntity, HsHostingAssetResource.class))) + .map(assetEntity -> ResponseEntity.ok( + mapper.map(assetEntity, HsHostingAssetResource.class))) .orElseGet(() -> ResponseEntity.notFound().build()); } @@ -94,10 +94,10 @@ public class HsHostingAssetController implements HsHostingAssetsApi { public ResponseEntity deleteAssetUuid( final String currentUser, final String assumedRoles, - final UUID serverUuid) { + final UUID assetUuid) { context.define(currentUser, assumedRoles); - final var result = assetRepo.deleteByUuid(serverUuid); + final var result = assetRepo.deleteByUuid(assetUuid); return result == 0 ? ResponseEntity.notFound().build() : ResponseEntity.noContent().build(); @@ -108,12 +108,12 @@ public class HsHostingAssetController implements HsHostingAssetsApi { public ResponseEntity patchAsset( final String currentUser, final String assumedRoles, - final UUID serverUuid, + final UUID assetUuid, final HsHostingAssetPatchResource body) { context.define(currentUser, assumedRoles); - final var current = assetRepo.findByUuid(serverUuid).orElseThrow(); + final var current = assetRepo.findByUuid(assetUuid).orElseThrow(); new HsHostingAssetEntityPatcher(current).apply(body); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java index 1f4ec01a..04a812a2 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java @@ -33,14 +33,11 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; -import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; -import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; -import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef.inCaseOf; -import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef.inOtherCases; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NULLABLE; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; @@ -79,11 +76,11 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validata @Version private int version; - @ManyToOne(optional = false) + @ManyToOne @JoinColumn(name = "bookingitemuuid") private HsBookingItemEntity bookingItem; - @ManyToOne(optional = true) + @ManyToOne @JoinColumn(name = "parentassetuuid") private HsHostingAssetEntity parentAsset; @@ -136,47 +133,39 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validata public static RbacView rbac() { return rbacViewFor("asset", HsHostingAssetEntity.class) - .withIdentityView(SQL.query(""" - SELECT asset.uuid as uuid, bookingItemIV.idName || '-' || cleanIdentifier(asset.identifier) as idName - FROM hs_hosting_asset asset - JOIN hs_booking_item_iv bookingItemIV ON bookingItemIV.uuid = asset.bookingItemUuid - """)) + .withIdentityView(SQL.projection("identifier")) .withRestrictedViewOrderBy(SQL.expression("identifier")) .withUpdatableColumns("version", "caption", "config") + .toRole(GLOBAL, ADMIN).grantPermission(INSERT) // TODO.impl: Why is this necessary to insert test data? .importEntityAlias("bookingItem", HsBookingItemEntity.class, usingDefaultCase(), dependsOnColumn("bookingItemUuid"), directlyFetchedByDependsOnColumn(), NULLABLE) + .toRole("bookingItem", AGENT).grantPermission(INSERT) - .switchOnColumn("type", - inCaseOf(CLOUD_SERVER.name(), - then -> then.toRole("bookingItem", AGENT).grantPermission(INSERT)), - inCaseOf(MANAGED_SERVER.name(), - then -> then.toRole("bookingItem", AGENT).grantPermission(INSERT)), - inCaseOf(MANAGED_WEBSPACE.name(), then -> - then.importEntityAlias("parentServer", HsHostingAssetEntity.class, usingCase(MANAGED_SERVER), - dependsOnColumn("parentAssetUuid"), - directlyFetchedByDependsOnColumn(), - NULLABLE) - .toRole("parentServer", ADMIN).grantPermission(INSERT) - .toRole("bookingItem", AGENT).grantPermission(INSERT) - ), - inOtherCases(then -> {}) - ) + .importEntityAlias("parentAsset", HsHostingAssetEntity.class, usingCase(MANAGED_SERVER), + dependsOnColumn("parentAssetUuid"), + directlyFetchedByDependsOnColumn(), + NULLABLE) + .toRole("parentAsset", ADMIN).grantPermission(INSERT) .createRole(OWNER, (with) -> { with.incomingSuperRole("bookingItem", ADMIN); + with.incomingSuperRole("parentAsset", ADMIN); with.permission(DELETE); }) .createSubRole(ADMIN, (with) -> { + with.incomingSuperRole("bookingItem", AGENT); + with.incomingSuperRole("parentAsset", AGENT); with.permission(UPDATE); }) + .createSubRole(AGENT) .createSubRole(TENANT, (with) -> { with.outgoingSubRole("bookingItem", TENANT); + with.outgoingSubRole("parentAsset", TENANT); with.permission(SELECT); }) - .limitDiagramTo("asset", "bookingItem", "bookingItem.debitorRel", "parentServer", "global"); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepository.java index 4926c673..7de7726b 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepository.java @@ -15,13 +15,13 @@ public interface HsHostingAssetRepository extends Repository findAllByCriteriaImpl(UUID debitorUuid, UUID parentAssetUuid, String type); - default List findAllByCriteria(final UUID debitorUuid, final UUID parentAssetUuid, final HsHostingAssetType type) { - return findAllByCriteriaImpl(debitorUuid, parentAssetUuid, HsHostingAssetType.asString(type)); + List findAllByCriteriaImpl(UUID projectUuid, UUID parentAssetUuid, String type); + default List findAllByCriteria(final UUID projectUuid, final UUID parentAssetUuid, final HsHostingAssetType type) { + return findAllByCriteriaImpl(projectUuid, parentAssetUuid, HsHostingAssetType.asString(type)); } HsHostingAssetEntity save(HsHostingAssetEntity current); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidator.java index 452bb116..116666fa 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidator.java @@ -26,7 +26,7 @@ class HsManagedWebspaceHostingAssetValidator extends HsEntityValidator result, final HsHostingAssetEntity assetEntity) { - final var expectedIdentifierPattern = "^" + assetEntity.getParentAsset().getBookingItem().getDebitor().getDefaultPrefix() + "[0-9][0-9]$"; + final var expectedIdentifierPattern = "^" + assetEntity.getParentAsset().getBookingItem().getProject().getDebitor().getDefaultPrefix() + "[0-9][0-9]$"; if ( !assetEntity.getIdentifier().matches(expectedIdentifierPattern)) { result.add("'identifier' expected to match '"+expectedIdentifierPattern+"', but is '" + assetEntity.getIdentifier() + "'"); } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java index b3c37bad..7c8b08ea 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java @@ -150,7 +150,7 @@ public class InsertTriggerGenerator { returns trigger language plpgsql as $$ begin - raise exception '[403] insert into ${rawSubTable} not allowed regardless of current subject, no insert permissions grated at all'; + raise exception '[403] insert into ${rawSubTable} values(%) not allowed regardless of current subject, no insert permissions granted at all', NEW; end; $$; create trigger ${rawSubTable}_insert_permission_check_tg @@ -254,8 +254,8 @@ public class InsertTriggerGenerator { private void generateInsertPermissionsChecksFooter(final StringWriter plPgSql) { plPgSql.writeLn(); plPgSql.writeLn(""" - raise exception '[403] insert into ${rawSubTable} not allowed for current subjects % (%)', - currentSubjects(), currentSubjectsUuids(); + raise exception '[403] insert into ${rawSubTable} values(%) not allowed for current subjects % (%)', + NEW, currentSubjects(), currentSubjectsUuids(); end; $$; create trigger ${rawSubTable}_insert_permission_check_tg diff --git a/src/main/resources/api-definition/hs-booking/api-mappings.yaml b/src/main/resources/api-definition/hs-booking/api-mappings.yaml index e16861f0..18f34c1f 100644 --- a/src/main/resources/api-definition/hs-booking/api-mappings.yaml +++ b/src/main/resources/api-definition/hs-booking/api-mappings.yaml @@ -13,5 +13,7 @@ map: - type: string:uuid => java.util.UUID paths: + /api/hs/booking/projects/{bookingProjectUuid}: + null: org.openapitools.jackson.nullable.JsonNullable /api/hs/booking/items/{bookingItemUuid}: null: org.openapitools.jackson.nullable.JsonNullable diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml index 25add552..aa7ab925 100644 --- a/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml +++ b/src/main/resources/api-definition/hs-booking/hs-booking-item-schemas.yaml @@ -51,7 +51,7 @@ components: HsBookingItemInsert: type: object properties: - debitorUuid: + projectUuid: type: string format: uuid nullable: false @@ -74,7 +74,7 @@ components: $ref: '#/components/schemas/BookingResources' required: - caption - - debitorUuid + - projectUuid - validFrom - resources additionalProperties: false diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-items.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-items.yaml index e869af21..40a3d010 100644 --- a/src/main/resources/api-definition/hs-booking/hs-booking-items.yaml +++ b/src/main/resources/api-definition/hs-booking/hs-booking-items.yaml @@ -1,19 +1,19 @@ get: - summary: Returns a list of all booking items for a specified debitor. - description: Returns the list of all booking items for a specified debitor which are visible to the current user or any of it's assumed roles. + summary: Returns a list of all booking items for a specified project. + description: Returns the list of all booking items for a specified project which are visible to the current user or any of it's assumed roles. tags: - hs-booking-items - operationId: listBookingItemsByDebitorUuid + operationId: listBookingItemsByProjectUuid parameters: - $ref: 'auth.yaml#/components/parameters/currentUser' - $ref: 'auth.yaml#/components/parameters/assumedRoles' - - name: debitorUuid + - name: projectUuid in: query required: true schema: type: string format: uuid - description: The UUID of the debitor, whose booking items are to be listed. + description: The UUID of the project, whose booking items are to be listed. responses: "200": description: OK diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-project-schemas.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-project-schemas.yaml new file mode 100644 index 00000000..de95203d --- /dev/null +++ b/src/main/resources/api-definition/hs-booking/hs-booking-project-schemas.yaml @@ -0,0 +1,40 @@ + +components: + + schemas: + + HsBookingProject: + type: object + properties: + uuid: + type: string + format: uuid + caption: + type: string + required: + - uuid + - caption + + HsBookingProjectPatch: + type: object + properties: + caption: + type: string + nullable: true + + HsBookingProjectInsert: + type: object + properties: + debitorUuid: + type: string + format: uuid + nullable: false + caption: + type: string + minLength: 3 + maxLength: 80 + nullable: false + required: + - debitorUuid + - caption + additionalProperties: false diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-projects-with-uuid.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-projects-with-uuid.yaml new file mode 100644 index 00000000..085205a7 --- /dev/null +++ b/src/main/resources/api-definition/hs-booking/hs-booking-projects-with-uuid.yaml @@ -0,0 +1,83 @@ +get: + tags: + - hs-booking-projects + description: 'Fetch a single booking project its uuid, if visible for the current subject.' + operationId: getBookingProjectByUuid + parameters: + - $ref: 'auth.yaml#/components/parameters/currentUser' + - $ref: 'auth.yaml#/components/parameters/assumedRoles' + - name: bookingProjectUuid + in: path + required: true + schema: + type: string + format: uuid + description: UUID of the booking project to fetch. + responses: + "200": + description: OK + content: + 'application/json': + schema: + $ref: 'hs-booking-project-schemas.yaml#/components/schemas/HsBookingProject' + + "401": + $ref: 'error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: 'error-responses.yaml#/components/responses/Forbidden' + +patch: + tags: + - hs-booking-projects + description: 'Updates a single booking project identified by its uuid, if permitted for the current subject.' + operationId: patchBookingProject + parameters: + - $ref: 'auth.yaml#/components/parameters/currentUser' + - $ref: 'auth.yaml#/components/parameters/assumedRoles' + - name: bookingProjectUuid + in: path + required: true + schema: + type: string + format: uuid + requestBody: + content: + 'application/json': + schema: + $ref: 'hs-booking-project-schemas.yaml#/components/schemas/HsBookingProjectPatch' + responses: + "200": + description: OK + content: + 'application/json': + schema: + $ref: 'hs-booking-project-schemas.yaml#/components/schemas/HsBookingProject' + "401": + $ref: 'error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: 'error-responses.yaml#/components/responses/Forbidden' + +delete: + tags: + - hs-booking-projects + description: 'Delete a single booking project identified by its uuid, if permitted for the current subject.' + operationId: deleteBookingIemByUuid + parameters: + - $ref: 'auth.yaml#/components/parameters/currentUser' + - $ref: 'auth.yaml#/components/parameters/assumedRoles' + - name: bookingProjectUuid + in: path + required: true + schema: + type: string + format: uuid + description: UUID of the booking project to delete. + responses: + "204": + description: No Content + "401": + $ref: 'error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: 'error-responses.yaml#/components/responses/Forbidden' + "404": + $ref: 'error-responses.yaml#/components/responses/NotFound' diff --git a/src/main/resources/api-definition/hs-booking/hs-booking-projects.yaml b/src/main/resources/api-definition/hs-booking/hs-booking-projects.yaml new file mode 100644 index 00000000..bccb7443 --- /dev/null +++ b/src/main/resources/api-definition/hs-booking/hs-booking-projects.yaml @@ -0,0 +1,58 @@ +get: + summary: Returns a list of all booking projects for a specified debitor. + description: Returns the list of all booking projects for a specified debitor which are visible to the current user or any of it's assumed roles. + tags: + - hs-booking-projects + operationId: listBookingProjectsByDebitorUuid + parameters: + - $ref: 'auth.yaml#/components/parameters/currentUser' + - $ref: 'auth.yaml#/components/parameters/assumedRoles' + - name: debitorUuid + in: query + required: true + schema: + type: string + format: uuid + description: The UUID of the debitor, whose booking projects are to be listed. + responses: + "200": + description: OK + content: + 'application/json': + schema: + type: array + items: + $ref: 'hs-booking-project-schemas.yaml#/components/schemas/HsBookingProject' + "401": + $ref: 'error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: 'error-responses.yaml#/components/responses/Forbidden' + +post: + summary: Adds a new project as a container for booking items. + tags: + - hs-booking-projects + operationId: addBookingProject + parameters: + - $ref: 'auth.yaml#/components/parameters/currentUser' + - $ref: 'auth.yaml#/components/parameters/assumedRoles' + requestBody: + description: A JSON object describing the new booking project. + required: true + content: + application/json: + schema: + $ref: 'hs-booking-project-schemas.yaml#/components/schemas/HsBookingProjectInsert' + responses: + "201": + description: Created + content: + 'application/json': + schema: + $ref: 'hs-booking-project-schemas.yaml#/components/schemas/HsBookingProject' + "401": + $ref: 'error-responses.yaml#/components/responses/Unauthorized' + "403": + $ref: 'error-responses.yaml#/components/responses/Forbidden' + "409": + $ref: 'error-responses.yaml#/components/responses/Conflict' diff --git a/src/main/resources/api-definition/hs-booking/hs-booking.yaml b/src/main/resources/api-definition/hs-booking/hs-booking.yaml index d6a67058..6faaf47c 100644 --- a/src/main/resources/api-definition/hs-booking/hs-booking.yaml +++ b/src/main/resources/api-definition/hs-booking/hs-booking.yaml @@ -8,6 +8,15 @@ servers: paths: + # Projects + + /api/hs/booking/projects: + $ref: "hs-booking-projects.yaml" + + /api/hs/booking/projects/{bookingProjectUuid}: + $ref: "hs-booking-projects-with-uuid.yaml" + + # Items /api/hs/booking/items: diff --git a/src/main/resources/db/changelog/6-hs-booking/610-booking-project/6100-hs-booking-project.sql b/src/main/resources/db/changelog/6-hs-booking/610-booking-project/6100-hs-booking-project.sql new file mode 100644 index 00000000..41fc650a --- /dev/null +++ b/src/main/resources/db/changelog/6-hs-booking/610-booking-project/6100-hs-booking-project.sql @@ -0,0 +1,22 @@ +--liquibase formatted sql + +-- ============================================================================ +--changeset booking-project-MAIN-TABLE:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +create table if not exists hs_booking_project +( + uuid uuid unique references RbacObject (uuid), + version int not null default 0, + debitorUuid uuid not null references hs_office_debitor(uuid), + caption varchar(80) not null +); +--// + + +-- ============================================================================ +--changeset hs-booking-project-MAIN-TABLE-JOURNAL:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +call create_journal('hs_booking_project'); +--// diff --git a/src/main/resources/db/changelog/6-hs-booking/610-booking-project/6103-hs-booking-project-rbac.md b/src/main/resources/db/changelog/6-hs-booking/610-booking-project/6103-hs-booking-project-rbac.md new file mode 100644 index 00000000..270908a8 --- /dev/null +++ b/src/main/resources/db/changelog/6-hs-booking/610-booking-project/6103-hs-booking-project-rbac.md @@ -0,0 +1,63 @@ +### rbac project + +This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually. + +```mermaid +%%{init:{'flowchart':{'htmlLabels':false}}}%% +flowchart TB + +subgraph debitorRel["`**debitorRel**`"] + direction TB + style debitorRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph debitorRel:roles[ ] + style debitorRel:roles fill:#99bcdb,stroke:white + + role:debitorRel:OWNER[[debitorRel:OWNER]] + role:debitorRel:ADMIN[[debitorRel:ADMIN]] + role:debitorRel:AGENT[[debitorRel:AGENT]] + role:debitorRel:TENANT[[debitorRel:TENANT]] + end +end + +subgraph project["`**project**`"] + direction TB + style project fill:#dd4901,stroke:#274d6e,stroke-width:8px + + subgraph project:roles[ ] + style project:roles fill:#dd4901,stroke:white + + role:project:OWNER[[project:OWNER]] + role:project:ADMIN[[project:ADMIN]] + role:project:AGENT[[project:AGENT]] + role:project:TENANT[[project:TENANT]] + end + + subgraph project:permissions[ ] + style project:permissions fill:#dd4901,stroke:white + + perm:project:INSERT{{project:INSERT}} + perm:project:DELETE{{project:DELETE}} + perm:project:UPDATE{{project:UPDATE}} + perm:project:SELECT{{project:SELECT}} + end +end + +%% granting roles to roles +role:global:ADMIN -.-> role:debitorRel:OWNER +role:debitorRel:OWNER -.-> role:debitorRel:ADMIN +role:debitorRel:ADMIN -.-> role:debitorRel:AGENT +role:debitorRel:AGENT -.-> role:debitorRel:TENANT +role:debitorRel:AGENT ==> role:project:OWNER +role:project:OWNER ==> role:project:ADMIN +role:project:ADMIN ==> role:project:AGENT +role:project:AGENT ==> role:project:TENANT +role:project:TENANT ==> role:debitorRel:TENANT + +%% granting permissions to roles +role:debitorRel:ADMIN ==> perm:project:INSERT +role:global:ADMIN ==> perm:project:DELETE +role:project:ADMIN ==> perm:project:UPDATE +role:project:TENANT ==> perm:project:SELECT + +``` diff --git a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql b/src/main/resources/db/changelog/6-hs-booking/610-booking-project/6103-hs-booking-project-rbac.sql similarity index 59% rename from src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql rename to src/main/resources/db/changelog/6-hs-booking/610-booking-project/6103-hs-booking-project-rbac.sql index e26edbbb..e0e0a9b7 100644 --- a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql +++ b/src/main/resources/db/changelog/6-hs-booking/610-booking-project/6103-hs-booking-project-rbac.sql @@ -3,29 +3,29 @@ -- ============================================================================ ---changeset hs-booking-item-rbac-OBJECT:1 endDelimiter:--// +--changeset hs-booking-project-rbac-OBJECT:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRelatedRbacObject('hs_booking_item'); +call generateRelatedRbacObject('hs_booking_project'); --// -- ============================================================================ ---changeset hs-booking-item-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--// +--changeset hs-booking-project-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacRoleDescriptors('hsBookingItem', 'hs_booking_item'); +call generateRbacRoleDescriptors('hsBookingProject', 'hs_booking_project'); --// -- ============================================================================ ---changeset hs-booking-item-rbac-insert-trigger:1 endDelimiter:--// +--changeset hs-booking-project-rbac-insert-trigger:1 endDelimiter:--// -- ---------------------------------------------------------------------------- /* Creates the roles, grants and permission for the AFTER INSERT TRIGGER. */ -create or replace procedure buildRbacSystemForHsBookingItem( - NEW hs_booking_item +create or replace procedure buildRbacSystemForHsBookingProject( + NEW hs_booking_project ) language plpgsql as $$ @@ -48,27 +48,25 @@ begin perform createRoleWithGrants( - hsBookingItemOWNER(NEW), + hsBookingProjectOWNER(NEW), incomingSuperRoles => array[hsOfficeRelationAGENT(newDebitorRel)] ); perform createRoleWithGrants( - hsBookingItemADMIN(NEW), + hsBookingProjectADMIN(NEW), permissions => array['UPDATE'], - incomingSuperRoles => array[ - hsBookingItemOWNER(NEW), - hsOfficeRelationAGENT(newDebitorRel)] + incomingSuperRoles => array[hsBookingProjectOWNER(NEW)] ); perform createRoleWithGrants( - hsBookingItemAGENT(NEW), - incomingSuperRoles => array[hsBookingItemADMIN(NEW)] + hsBookingProjectAGENT(NEW), + incomingSuperRoles => array[hsBookingProjectADMIN(NEW)] ); perform createRoleWithGrants( - hsBookingItemTENANT(NEW), + hsBookingProjectTENANT(NEW), permissions => array['SELECT'], - incomingSuperRoles => array[hsBookingItemAGENT(NEW)], + incomingSuperRoles => array[hsBookingProjectAGENT(NEW)], outgoingSubRoles => array[hsOfficeRelationTENANT(newDebitorRel)] ); @@ -78,81 +76,81 @@ begin end; $$; /* - AFTER INSERT TRIGGER to create the role+grant structure for a new hs_booking_item row. + AFTER INSERT TRIGGER to create the role+grant structure for a new hs_booking_project row. */ -create or replace function insertTriggerForHsBookingItem_tf() +create or replace function insertTriggerForHsBookingProject_tf() returns trigger language plpgsql strict as $$ begin - call buildRbacSystemForHsBookingItem(NEW); + call buildRbacSystemForHsBookingProject(NEW); return NEW; end; $$; -create trigger insertTriggerForHsBookingItem_tg - after insert on hs_booking_item +create trigger insertTriggerForHsBookingProject_tg + after insert on hs_booking_project for each row -execute procedure insertTriggerForHsBookingItem_tf(); +execute procedure insertTriggerForHsBookingProject_tf(); --// -- ============================================================================ ---changeset hs-booking-item-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--// +--changeset hs-booking-project-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -- granting INSERT permission to hs_office_relation ---------------------------- /* - Grants INSERT INTO hs_booking_item permissions to specified role of pre-existing hs_office_relation rows. + Grants INSERT INTO hs_booking_project permissions to specified role of pre-existing hs_office_relation rows. */ do language plpgsql $$ declare row hs_office_relation; begin - call defineContext('create INSERT INTO hs_booking_item permissions for pre-exising hs_office_relation rows'); + call defineContext('create INSERT INTO hs_booking_project permissions for pre-exising hs_office_relation rows'); FOR row IN SELECT * FROM hs_office_relation WHERE type = 'DEBITOR' LOOP call grantPermissionToRole( - createPermission(row.uuid, 'INSERT', 'hs_booking_item'), + createPermission(row.uuid, 'INSERT', 'hs_booking_project'), hsOfficeRelationADMIN(row)); END LOOP; end; $$; /** - Grants hs_booking_item INSERT permission to specified role of new hs_office_relation rows. + Grants hs_booking_project INSERT permission to specified role of new hs_office_relation rows. */ -create or replace function new_hs_booking_item_grants_insert_to_hs_office_relation_tf() +create or replace function new_hs_booking_project_grants_insert_to_hs_office_relation_tf() returns trigger language plpgsql strict as $$ begin if NEW.type = 'DEBITOR' then call grantPermissionToRole( - createPermission(NEW.uuid, 'INSERT', 'hs_booking_item'), + createPermission(NEW.uuid, 'INSERT', 'hs_booking_project'), hsOfficeRelationADMIN(NEW)); end if; return NEW; end; $$; -- z_... is to put it at the end of after insert triggers, to make sure the roles exist -create trigger z_new_hs_booking_item_grants_insert_to_hs_office_relation_tg +create trigger z_new_hs_booking_project_grants_insert_to_hs_office_relation_tg after insert on hs_office_relation for each row -execute procedure new_hs_booking_item_grants_insert_to_hs_office_relation_tf(); +execute procedure new_hs_booking_project_grants_insert_to_hs_office_relation_tf(); -- ============================================================================ ---changeset hs_booking_item-rbac-CHECKING-INSERT-PERMISSION:1 endDelimiter:--// +--changeset hs_booking_project-rbac-CHECKING-INSERT-PERMISSION:1 endDelimiter:--// -- ---------------------------------------------------------------------------- /** - Checks if the user respectively the assumed roles are allowed to insert a row to hs_booking_item. + Checks if the user respectively the assumed roles are allowed to insert a row to hs_booking_project. */ -create or replace function hs_booking_item_insert_permission_check_tf() +create or replace function hs_booking_project_insert_permission_check_tf() returns trigger language plpgsql as $$ declare @@ -164,47 +162,45 @@ begin JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid WHERE debitor.uuid = NEW.debitorUuid ); - assert superObjectUuid is not null, 'object uuid fetched depending on hs_booking_item.debitorUuid must not be null, also check fetchSql in RBAC DSL'; - if hasInsertPermission(superObjectUuid, 'hs_booking_item') then + assert superObjectUuid is not null, 'object uuid fetched depending on hs_booking_project.debitorUuid must not be null, also check fetchSql in RBAC DSL'; + if hasInsertPermission(superObjectUuid, 'hs_booking_project') then return NEW; end if; - raise exception '[403] insert into hs_booking_item not allowed for current subjects % (%)', - currentSubjects(), currentSubjectsUuids(); + raise exception '[403] insert into hs_booking_project values(%) not allowed for current subjects % (%)', + NEW, currentSubjects(), currentSubjectsUuids(); end; $$; -create trigger hs_booking_item_insert_permission_check_tg - before insert on hs_booking_item +create trigger hs_booking_project_insert_permission_check_tg + before insert on hs_booking_project for each row - execute procedure hs_booking_item_insert_permission_check_tf(); + execute procedure hs_booking_project_insert_permission_check_tf(); --// -- ============================================================================ ---changeset hs-booking-item-rbac-IDENTITY-VIEW:1 endDelimiter:--// +--changeset hs-booking-project-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityViewFromQuery('hs_booking_item', +call generateRbacIdentityViewFromQuery('hs_booking_project', $idName$ - SELECT bookingItem.uuid as uuid, debitorIV.idName || '-' || cleanIdentifier(bookingItem.caption) as idName - FROM hs_booking_item bookingItem - JOIN hs_office_debitor_iv debitorIV ON debitorIV.uuid = bookingItem.debitorUuid + SELECT bookingProject.uuid as uuid, debitorIV.idName || '-' || cleanIdentifier(bookingProject.caption) as idName + FROM hs_booking_project bookingProject + JOIN hs_office_debitor_iv debitorIV ON debitorIV.uuid = bookingProject.debitorUuid $idName$); --// -- ============================================================================ ---changeset hs-booking-item-rbac-RESTRICTED-VIEW:1 endDelimiter:--// +--changeset hs-booking-project-rbac-RESTRICTED-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacRestrictedView('hs_booking_item', +call generateRbacRestrictedView('hs_booking_project', $orderBy$ - validity + caption $orderBy$, $updates$ version = new.version, - caption = new.caption, - validity = new.validity, - resources = new.resources + caption = new.caption $updates$); --// diff --git a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6018-hs-booking-item-test-data.sql b/src/main/resources/db/changelog/6-hs-booking/610-booking-project/6108-hs-booking-project-test-data.sql similarity index 51% rename from src/main/resources/db/changelog/6-hs-booking/601-booking-item/6018-hs-booking-item-test-data.sql rename to src/main/resources/db/changelog/6-hs-booking/610-booking-project/6108-hs-booking-project-test-data.sql index 88ada16f..5ebae299 100644 --- a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6018-hs-booking-item-test-data.sql +++ b/src/main/resources/db/changelog/6-hs-booking/610-booking-project/6108-hs-booking-project-test-data.sql @@ -2,13 +2,13 @@ -- ============================================================================ ---changeset hs-booking-item-TEST-DATA-GENERATOR:1 endDelimiter:--// +--changeset hs-booking-project-TEST-DATA-GENERATOR:1 endDelimiter:--// -- ---------------------------------------------------------------------------- /* - Creates a single hs_booking_item test record. + Creates a single hs_booking_project test record. */ -create or replace procedure createHsBookingItemTransactionTestData( +create or replace procedure createHsBookingProjectTransactionTestData( givenPartnerNumber numeric, givenDebitorSuffix char(2) ) @@ -17,7 +17,7 @@ declare currentTask varchar; relatedDebitor hs_office_debitor; begin - currentTask := 'creating booking-item test-data ' || givenPartnerNumber::text || givenDebitorSuffix; + currentTask := 'creating booking-project test-data ' || givenPartnerNumber::text || givenDebitorSuffix; call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN'); execute format('set local hsadminng.currentTask to %L', currentTask); @@ -28,26 +28,24 @@ begin join hs_office_partner partner on partner.partnerRelUuid = partnerRel.uuid where partner.partnerNumber = givenPartnerNumber and debitor.debitorNumberSuffix = givenDebitorSuffix; - raise notice 'creating test booking-item: %', givenPartnerNumber::text || givenDebitorSuffix::text; + raise notice 'creating test booking-project: %', givenDebitorSuffix::text; raise notice '- using debitor (%): %', relatedDebitor.uuid, relatedDebitor; insert - into hs_booking_item (uuid, debitoruuid, type, caption, validity, resources) - values (uuid_generate_v4(), relatedDebitor.uuid, 'MANAGED_SERVER', 'some ManagedServer', daterange('20221001', null, '[]'), '{ "CPUs": 2, "RAM": 8, "SDD": 512, "Traffic": 42 }'::jsonb), - (uuid_generate_v4(), relatedDebitor.uuid, 'CLOUD_SERVER', 'some CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 2, "RAM": 4, "HDD": 1024, "Traffic": 42 }'::jsonb), - (uuid_generate_v4(), relatedDebitor.uuid, 'PRIVATE_CLOUD', 'some PrivateCloud', daterange('20240401', null, '[]'), '{ "CPUs": 10, "SDD": 10240, "HDD": 10240, "Traffic": 42 }'::jsonb); + into hs_booking_project (uuid, debitoruuid, caption) + values (uuid_generate_v4(), relatedDebitor.uuid, 'D-' || givenPartnerNumber::text || givenDebitorSuffix || ' default project'); end; $$; --// -- ============================================================================ ---changeset hs-booking-item-TEST-DATA-GENERATION:1 –context=dev,tc endDelimiter:--// +--changeset hs-booking-project-TEST-DATA-GENERATION:1 –context=dev,tc endDelimiter:--// -- ---------------------------------------------------------------------------- do language plpgsql $$ begin - call createHsBookingItemTransactionTestData(10001, '11'); - call createHsBookingItemTransactionTestData(10002, '12'); - call createHsBookingItemTransactionTestData(10003, '13'); + call createHsBookingProjectTransactionTestData(10001, '11'); + call createHsBookingProjectTransactionTestData(10002, '12'); + call createHsBookingProjectTransactionTestData(10003, '13'); end; $$; --// diff --git a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6010-hs-booking-item.sql b/src/main/resources/db/changelog/6-hs-booking/620-booking-item/6200-hs-booking-item.sql similarity index 75% rename from src/main/resources/db/changelog/6-hs-booking/601-booking-item/6010-hs-booking-item.sql rename to src/main/resources/db/changelog/6-hs-booking/620-booking-item/6200-hs-booking-item.sql index d63e317e..6c76c29f 100644 --- a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6010-hs-booking-item.sql +++ b/src/main/resources/db/changelog/6-hs-booking/620-booking-item/6200-hs-booking-item.sql @@ -17,11 +17,15 @@ create table if not exists hs_booking_item ( uuid uuid unique references RbacObject (uuid), version int not null default 0, - debitorUuid uuid not null references hs_office_debitor(uuid), + projectUuid uuid null references hs_booking_project(uuid), type HsBookingItemType not null, + parentItemUuid uuid null references hs_booking_item(uuid) initially deferred, validity daterange not null, caption varchar(80) not null, - resources jsonb not null + resources jsonb not null, + + constraint chk_hs_booking_item_has_project_or_parent_asset + check (projectUuid is not null or parentItemUuid is not null) ); --// diff --git a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.md b/src/main/resources/db/changelog/6-hs-booking/620-booking-item/6203-hs-booking-item-rbac.md similarity index 63% rename from src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.md rename to src/main/resources/db/changelog/6-hs-booking/620-booking-item/6203-hs-booking-item-rbac.md index 7ba21f5c..4775616f 100644 --- a/src/main/resources/db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.md +++ b/src/main/resources/db/changelog/6-hs-booking/620-booking-item/6203-hs-booking-item-rbac.md @@ -29,35 +29,34 @@ subgraph bookingItem["`**bookingItem**`"] end end -subgraph debitorRel["`**debitorRel**`"] +subgraph project["`**project**`"] direction TB - style debitorRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px + style project fill:#99bcdb,stroke:#274d6e,stroke-width:8px - subgraph debitorRel:roles[ ] - style debitorRel:roles fill:#99bcdb,stroke:white + subgraph project:roles[ ] + style project:roles fill:#99bcdb,stroke:white - role:debitorRel:OWNER[[debitorRel:OWNER]] - role:debitorRel:ADMIN[[debitorRel:ADMIN]] - role:debitorRel:AGENT[[debitorRel:AGENT]] - role:debitorRel:TENANT[[debitorRel:TENANT]] + role:project:OWNER[[project:OWNER]] + role:project:ADMIN[[project:ADMIN]] + role:project:AGENT[[project:AGENT]] + role:project:TENANT[[project:TENANT]] end end %% granting roles to roles -role:global:ADMIN -.-> role:debitorRel:OWNER -role:debitorRel:OWNER -.-> role:debitorRel:ADMIN -role:debitorRel:ADMIN -.-> role:debitorRel:AGENT -role:debitorRel:AGENT -.-> role:debitorRel:TENANT -role:debitorRel:AGENT ==> role:bookingItem:OWNER +role:project:OWNER -.-> role:project:ADMIN +role:project:ADMIN -.-> role:project:AGENT +role:project:AGENT -.-> role:project:TENANT +role:project:AGENT ==> role:bookingItem:OWNER role:bookingItem:OWNER ==> role:bookingItem:ADMIN -role:debitorRel:AGENT ==> role:bookingItem:ADMIN role:bookingItem:ADMIN ==> role:bookingItem:AGENT role:bookingItem:AGENT ==> role:bookingItem:TENANT -role:bookingItem:TENANT ==> role:debitorRel:TENANT +role:bookingItem:TENANT ==> role:project:TENANT %% granting permissions to roles -role:debitorRel:ADMIN ==> perm:bookingItem:INSERT +role:global:ADMIN ==> perm:bookingItem:INSERT role:global:ADMIN ==> perm:bookingItem:DELETE +role:project:ADMIN ==> perm:bookingItem:INSERT role:bookingItem:ADMIN ==> perm:bookingItem:UPDATE role:bookingItem:TENANT ==> perm:bookingItem:SELECT diff --git a/src/main/resources/db/changelog/6-hs-booking/620-booking-item/6203-hs-booking-item-rbac.sql b/src/main/resources/db/changelog/6-hs-booking/620-booking-item/6203-hs-booking-item-rbac.sql new file mode 100644 index 00000000..bcd6523e --- /dev/null +++ b/src/main/resources/db/changelog/6-hs-booking/620-booking-item/6203-hs-booking-item-rbac.sql @@ -0,0 +1,277 @@ +--liquibase formatted sql +-- This code generated was by RbacViewPostgresGenerator, do not amend manually. + + +-- ============================================================================ +--changeset hs-booking-item-rbac-OBJECT:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- +call generateRelatedRbacObject('hs_booking_item'); +--// + + +-- ============================================================================ +--changeset hs-booking-item-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- +call generateRbacRoleDescriptors('hsBookingItem', 'hs_booking_item'); +--// + + +-- ============================================================================ +--changeset hs-booking-item-rbac-insert-trigger:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/* + Creates the roles, grants and permission for the AFTER INSERT TRIGGER. + */ + +create or replace procedure buildRbacSystemForHsBookingItem( + NEW hs_booking_item +) + language plpgsql as $$ + +declare + newProject hs_booking_project; + newParentItem hs_booking_item; + +begin + call enterTriggerForObjectUuid(NEW.uuid); + + SELECT * FROM hs_booking_project WHERE uuid = NEW.projectUuid INTO newProject; + + SELECT * FROM hs_booking_item WHERE uuid = NEW.parentItemUuid INTO newParentItem; + + perform createRoleWithGrants( + hsBookingItemOWNER(NEW), + incomingSuperRoles => array[ + hsBookingItemAGENT(newParentItem), + hsBookingProjectAGENT(newProject)] + ); + + perform createRoleWithGrants( + hsBookingItemADMIN(NEW), + permissions => array['UPDATE'], + incomingSuperRoles => array[hsBookingItemOWNER(NEW)] + ); + + perform createRoleWithGrants( + hsBookingItemAGENT(NEW), + incomingSuperRoles => array[hsBookingItemADMIN(NEW)] + ); + + perform createRoleWithGrants( + hsBookingItemTENANT(NEW), + permissions => array['SELECT'], + incomingSuperRoles => array[hsBookingItemAGENT(NEW)], + outgoingSubRoles => array[ + hsBookingItemTENANT(newParentItem), + hsBookingProjectTENANT(newProject)] + ); + + + + call grantPermissionToRole(createPermission(NEW.uuid, 'DELETE'), globalAdmin()); + + call leaveTriggerForObjectUuid(NEW.uuid); +end; $$; + +/* + AFTER INSERT TRIGGER to create the role+grant structure for a new hs_booking_item row. + */ + +create or replace function insertTriggerForHsBookingItem_tf() + returns trigger + language plpgsql + strict as $$ +begin + call buildRbacSystemForHsBookingItem(NEW); + return NEW; +end; $$; + +create trigger insertTriggerForHsBookingItem_tg + after insert on hs_booking_item + for each row +execute procedure insertTriggerForHsBookingItem_tf(); +--// + + +-- ============================================================================ +--changeset hs-booking-item-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +-- granting INSERT permission to global ---------------------------- + +/* + Grants INSERT INTO hs_booking_item permissions to specified role of pre-existing global rows. + */ +do language plpgsql $$ + declare + row global; + begin + call defineContext('create INSERT INTO hs_booking_item permissions for pre-exising global rows'); + + FOR row IN SELECT * FROM global + -- unconditional for all rows in that table + LOOP + call grantPermissionToRole( + createPermission(row.uuid, 'INSERT', 'hs_booking_item'), + globalADMIN()); + END LOOP; + end; +$$; + +/** + Grants hs_booking_item INSERT permission to specified role of new global rows. +*/ +create or replace function new_hs_booking_item_grants_insert_to_global_tf() + returns trigger + language plpgsql + strict as $$ +begin + -- unconditional for all rows in that table + call grantPermissionToRole( + createPermission(NEW.uuid, 'INSERT', 'hs_booking_item'), + globalADMIN()); + -- end. + return NEW; +end; $$; + +-- z_... is to put it at the end of after insert triggers, to make sure the roles exist +create trigger z_new_hs_booking_item_grants_insert_to_global_tg + after insert on global + for each row +execute procedure new_hs_booking_item_grants_insert_to_global_tf(); + +-- granting INSERT permission to hs_booking_project ---------------------------- + +/* + Grants INSERT INTO hs_booking_item permissions to specified role of pre-existing hs_booking_project rows. + */ +do language plpgsql $$ + declare + row hs_booking_project; + begin + call defineContext('create INSERT INTO hs_booking_item permissions for pre-exising hs_booking_project rows'); + + FOR row IN SELECT * FROM hs_booking_project + -- unconditional for all rows in that table + LOOP + call grantPermissionToRole( + createPermission(row.uuid, 'INSERT', 'hs_booking_item'), + hsBookingProjectADMIN(row)); + END LOOP; + end; +$$; + +/** + Grants hs_booking_item INSERT permission to specified role of new hs_booking_project rows. +*/ +create or replace function new_hs_booking_item_grants_insert_to_hs_booking_project_tf() + returns trigger + language plpgsql + strict as $$ +begin + -- unconditional for all rows in that table + call grantPermissionToRole( + createPermission(NEW.uuid, 'INSERT', 'hs_booking_item'), + hsBookingProjectADMIN(NEW)); + -- end. + return NEW; +end; $$; + +-- z_... is to put it at the end of after insert triggers, to make sure the roles exist +create trigger z_new_hs_booking_item_grants_insert_to_hs_booking_project_tg + after insert on hs_booking_project + for each row +execute procedure new_hs_booking_item_grants_insert_to_hs_booking_project_tf(); + +-- granting INSERT permission to hs_booking_item ---------------------------- + +-- Granting INSERT INTO hs_hosting_asset permissions to specified role of pre-existing hs_hosting_asset rows slipped, +-- because there cannot yet be any pre-existing rows in the same table yet. + +/** + Grants hs_booking_item INSERT permission to specified role of new hs_booking_item rows. +*/ +create or replace function new_hs_booking_item_grants_insert_to_hs_booking_item_tf() + returns trigger + language plpgsql + strict as $$ +begin + -- unconditional for all rows in that table + call grantPermissionToRole( + createPermission(NEW.uuid, 'INSERT', 'hs_booking_item'), + hsBookingItemADMIN(NEW)); + -- end. + return NEW; +end; $$; + +-- z_... is to put it at the end of after insert triggers, to make sure the roles exist +create trigger z_new_hs_booking_item_grants_insert_to_hs_booking_item_tg + after insert on hs_booking_item + for each row +execute procedure new_hs_booking_item_grants_insert_to_hs_booking_item_tf(); + + +-- ============================================================================ +--changeset hs_booking_item-rbac-CHECKING-INSERT-PERMISSION:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/** + Checks if the user respectively the assumed roles are allowed to insert a row to hs_booking_item. +*/ +create or replace function hs_booking_item_insert_permission_check_tf() + returns trigger + language plpgsql as $$ +declare + superObjectUuid uuid; +begin + -- check INSERT INSERT if global ADMIN + if isGlobalAdmin() then + return NEW; + end if; + -- check INSERT permission via direct foreign key: NEW.projectUuid + if hasInsertPermission(NEW.projectUuid, 'hs_booking_item') then + return NEW; + end if; + -- check INSERT permission via direct foreign key: NEW.parentItemUuid + if hasInsertPermission(NEW.parentItemUuid, 'hs_booking_item') then + return NEW; + end if; + + raise exception '[403] insert into hs_booking_item values(%) not allowed for current subjects % (%)', + NEW, currentSubjects(), currentSubjectsUuids(); +end; $$; + +create trigger hs_booking_item_insert_permission_check_tg + before insert on hs_booking_item + for each row + execute procedure hs_booking_item_insert_permission_check_tf(); +--// + + +-- ============================================================================ +--changeset hs-booking-item-rbac-IDENTITY-VIEW:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +call generateRbacIdentityViewFromProjection('hs_booking_item', + $idName$ + caption + $idName$); +--// + + +-- ============================================================================ +--changeset hs-booking-item-rbac-RESTRICTED-VIEW:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- +call generateRbacRestrictedView('hs_booking_item', + $orderBy$ + validity + $orderBy$, + $updates$ + version = new.version, + caption = new.caption, + validity = new.validity, + resources = new.resources + $updates$); +--// + diff --git a/src/main/resources/db/changelog/6-hs-booking/620-booking-item/6208-hs-booking-item-test-data.sql b/src/main/resources/db/changelog/6-hs-booking/620-booking-item/6208-hs-booking-item-test-data.sql new file mode 100644 index 00000000..bc3a9e51 --- /dev/null +++ b/src/main/resources/db/changelog/6-hs-booking/620-booking-item/6208-hs-booking-item-test-data.sql @@ -0,0 +1,58 @@ +--liquibase formatted sql + + +-- ============================================================================ +--changeset hs-booking-item-TEST-DATA-GENERATOR:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/* + Creates a single hs_booking_item test record. + */ +create or replace procedure createHsBookingItemTransactionTestData( + givenPartnerNumber numeric, + givenDebitorSuffix char(2) + ) + language plpgsql as $$ +declare + currentTask varchar; + relatedProject hs_booking_project; + privateCloudUuid uuid; + managedServerUuid uuid; +begin + currentTask := 'creating booking-item test-data ' || givenPartnerNumber::text || givenDebitorSuffix; + call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN'); + execute format('set local hsadminng.currentTask to %L', currentTask); + + select project.* into relatedProject + from hs_booking_project project + where project.caption = 'D-' || givenPartnerNumber || givenDebitorSuffix || ' default project'; + + raise notice 'creating test booking-item: %', givenPartnerNumber::text || givenDebitorSuffix::text; + raise notice '- using project (%): %', relatedProject.uuid, relatedProject; + privateCloudUuid := uuid_generate_v4(); + managedServerUuid := uuid_generate_v4(); + insert + into hs_booking_item (uuid, projectuuid, type, parentitemuuid, caption, validity, resources) + values (privateCloudUuid, relatedProject.uuid, 'PRIVATE_CLOUD', null, 'some PrivateCloud', daterange('20240401', null, '[]'), '{ "CPUs": 10, "SDD": 10240, "HDD": 10240, "Traffic": 42 }'::jsonb), + (uuid_generate_v4(), null, 'MANAGED_SERVER', privateCloudUuid, 'some ManagedServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 2, "RAM": 4, "HDD": 1024, "Traffic": 42 }'::jsonb), + (uuid_generate_v4(), null, 'CLOUD_SERVER', privateCloudUuid, 'test CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 2, "RAM": 4, "HDD": 1024, "Traffic": 42 }'::jsonb), + (uuid_generate_v4(), null, 'CLOUD_SERVER', privateCloudUuid, 'prod CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 4, "RAM": 16, "HDD": 2924, "Traffic": 420 }'::jsonb), + (managedServerUuid, relatedProject.uuid, 'MANAGED_SERVER', null, 'separate ManagedServer', daterange('20221001', null, '[]'), '{ "CPUs": 2, "RAM": 8, "SDD": 512, "Traffic": 42 }'::jsonb), + (uuid_generate_v4(), null, 'MANAGED_WEBSPACE', managedServerUuid, 'some ManagedWebspace', daterange('20221001', null, '[]'), '{ "SDD": 512, "Traffic": 12, "Daemons": 2, "Multi": 4 }'::jsonb), + (uuid_generate_v4(), relatedProject.uuid, 'MANAGED_WEBSPACE', null, 'some ManagedWebspace', daterange('20221001', null, '[]'), '{ "SDD": 512, "Traffic": 12, "Daemons": 2, "Multi": 4 }'::jsonb); +end; $$; +--// + + +-- ============================================================================ +--changeset hs-booking-item-TEST-DATA-GENERATION:1 –context=dev,tc endDelimiter:--// +-- ---------------------------------------------------------------------------- + +do language plpgsql $$ + begin + call createHsBookingItemTransactionTestData(10001, '11'); + call createHsBookingItemTransactionTestData(10002, '12'); + call createHsBookingItemTransactionTestData(10003, '13'); + end; +$$; +--// diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql index 4aa9e099..755dbbec 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql @@ -26,12 +26,13 @@ create table if not exists hs_hosting_asset version int not null default 0, bookingItemUuid uuid null references hs_booking_item(uuid), type HsHostingAssetType not null, - parentAssetUuid uuid null references hs_hosting_asset(uuid), + parentAssetUuid uuid null references hs_hosting_asset(uuid) initially deferred, identifier varchar(80) not null, - caption varchar(80) not null, + caption varchar(80), config jsonb not null, - constraint chk_hs_hosting_asset_has_booking_item_or_parent_asset check (bookingItemUuid is not null or parentAssetUuid is not null) + constraint chk_hs_hosting_asset_has_booking_item_or_parent_asset + check (bookingItemUuid is not null or parentAssetUuid is not null) ); --// diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac-CLOUD_SERVER.md b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac-CLOUD_SERVER.md index 65ae6608..c4abe818 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac-CLOUD_SERVER.md +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac-CLOUD_SERVER.md @@ -42,20 +42,6 @@ subgraph bookingItem["`**bookingItem**`"] end end -subgraph bookingItem.debitorRel["`**bookingItem.debitorRel**`"] - direction TB - style bookingItem.debitorRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px - - subgraph bookingItem.debitorRel:roles[ ] - style bookingItem.debitorRel:roles fill:#99bcdb,stroke:white - - role:bookingItem.debitorRel:OWNER[[bookingItem.debitorRel:OWNER]] - role:bookingItem.debitorRel:ADMIN[[bookingItem.debitorRel:ADMIN]] - role:bookingItem.debitorRel:AGENT[[bookingItem.debitorRel:AGENT]] - role:bookingItem.debitorRel:TENANT[[bookingItem.debitorRel:TENANT]] - end -end - subgraph parentServer["`**parentServer**`"] direction TB style parentServer fill:#99bcdb,stroke:#274d6e,stroke-width:8px @@ -68,16 +54,9 @@ subgraph parentServer["`**parentServer**`"] end %% granting roles to roles -role:global:ADMIN -.-> role:bookingItem.debitorRel:OWNER -role:bookingItem.debitorRel:OWNER -.-> role:bookingItem.debitorRel:ADMIN -role:bookingItem.debitorRel:ADMIN -.-> role:bookingItem.debitorRel:AGENT -role:bookingItem.debitorRel:AGENT -.-> role:bookingItem.debitorRel:TENANT -role:bookingItem.debitorRel:AGENT -.-> role:bookingItem:OWNER role:bookingItem:OWNER -.-> role:bookingItem:ADMIN -role:bookingItem.debitorRel:AGENT -.-> role:bookingItem:ADMIN role:bookingItem:ADMIN -.-> role:bookingItem:AGENT role:bookingItem:AGENT -.-> role:bookingItem:TENANT -role:bookingItem:TENANT -.-> role:bookingItem.debitorRel:TENANT role:bookingItem:ADMIN ==> role:asset:OWNER role:asset:OWNER ==> role:asset:ADMIN role:asset:ADMIN ==> role:asset:TENANT @@ -88,5 +67,6 @@ role:bookingItem:AGENT ==> perm:asset:INSERT role:asset:OWNER ==> perm:asset:DELETE role:asset:ADMIN ==> perm:asset:UPDATE role:asset:TENANT ==> perm:asset:SELECT +role:global:ADMIN ==> perm:asset:INSERT ``` diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac-MANAGED_SERVER.md b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac-MANAGED_SERVER.md index 773ae411..5d9b4710 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac-MANAGED_SERVER.md +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac-MANAGED_SERVER.md @@ -42,20 +42,6 @@ subgraph bookingItem["`**bookingItem**`"] end end -subgraph bookingItem.debitorRel["`**bookingItem.debitorRel**`"] - direction TB - style bookingItem.debitorRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px - - subgraph bookingItem.debitorRel:roles[ ] - style bookingItem.debitorRel:roles fill:#99bcdb,stroke:white - - role:bookingItem.debitorRel:OWNER[[bookingItem.debitorRel:OWNER]] - role:bookingItem.debitorRel:ADMIN[[bookingItem.debitorRel:ADMIN]] - role:bookingItem.debitorRel:AGENT[[bookingItem.debitorRel:AGENT]] - role:bookingItem.debitorRel:TENANT[[bookingItem.debitorRel:TENANT]] - end -end - subgraph parentServer["`**parentServer**`"] direction TB style parentServer fill:#99bcdb,stroke:#274d6e,stroke-width:8px @@ -68,16 +54,9 @@ subgraph parentServer["`**parentServer**`"] end %% granting roles to roles -role:global:ADMIN -.-> role:bookingItem.debitorRel:OWNER -role:bookingItem.debitorRel:OWNER -.-> role:bookingItem.debitorRel:ADMIN -role:bookingItem.debitorRel:ADMIN -.-> role:bookingItem.debitorRel:AGENT -role:bookingItem.debitorRel:AGENT -.-> role:bookingItem.debitorRel:TENANT -role:bookingItem.debitorRel:AGENT -.-> role:bookingItem:OWNER role:bookingItem:OWNER -.-> role:bookingItem:ADMIN -role:bookingItem.debitorRel:AGENT -.-> role:bookingItem:ADMIN role:bookingItem:ADMIN -.-> role:bookingItem:AGENT role:bookingItem:AGENT -.-> role:bookingItem:TENANT -role:bookingItem:TENANT -.-> role:bookingItem.debitorRel:TENANT role:bookingItem:ADMIN ==> role:asset:OWNER role:asset:OWNER ==> role:asset:ADMIN role:asset:ADMIN ==> role:asset:TENANT @@ -88,5 +67,6 @@ role:bookingItem:AGENT ==> perm:asset:INSERT role:asset:OWNER ==> perm:asset:DELETE role:asset:ADMIN ==> perm:asset:UPDATE role:asset:TENANT ==> perm:asset:SELECT +role:global:ADMIN ==> perm:asset:INSERT ``` diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac-MANAGED_WEBSPACE.md b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac-MANAGED_WEBSPACE.md index e9b929a9..5a35b108 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac-MANAGED_WEBSPACE.md +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac-MANAGED_WEBSPACE.md @@ -42,20 +42,6 @@ subgraph bookingItem["`**bookingItem**`"] end end -subgraph bookingItem.debitorRel["`**bookingItem.debitorRel**`"] - direction TB - style bookingItem.debitorRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px - - subgraph bookingItem.debitorRel:roles[ ] - style bookingItem.debitorRel:roles fill:#99bcdb,stroke:white - - role:bookingItem.debitorRel:OWNER[[bookingItem.debitorRel:OWNER]] - role:bookingItem.debitorRel:ADMIN[[bookingItem.debitorRel:ADMIN]] - role:bookingItem.debitorRel:AGENT[[bookingItem.debitorRel:AGENT]] - role:bookingItem.debitorRel:TENANT[[bookingItem.debitorRel:TENANT]] - end -end - subgraph parentServer["`**parentServer**`"] direction TB style parentServer fill:#99bcdb,stroke:#274d6e,stroke-width:8px @@ -68,16 +54,9 @@ subgraph parentServer["`**parentServer**`"] end %% granting roles to roles -role:global:ADMIN -.-> role:bookingItem.debitorRel:OWNER -role:bookingItem.debitorRel:OWNER -.-> role:bookingItem.debitorRel:ADMIN -role:bookingItem.debitorRel:ADMIN -.-> role:bookingItem.debitorRel:AGENT -role:bookingItem.debitorRel:AGENT -.-> role:bookingItem.debitorRel:TENANT -role:bookingItem.debitorRel:AGENT -.-> role:bookingItem:OWNER role:bookingItem:OWNER -.-> role:bookingItem:ADMIN -role:bookingItem.debitorRel:AGENT -.-> role:bookingItem:ADMIN role:bookingItem:ADMIN -.-> role:bookingItem:AGENT role:bookingItem:AGENT -.-> role:bookingItem:TENANT -role:bookingItem:TENANT -.-> role:bookingItem.debitorRel:TENANT role:bookingItem:ADMIN ==> role:asset:OWNER role:asset:OWNER ==> role:asset:ADMIN role:asset:ADMIN ==> role:asset:TENANT @@ -89,5 +68,6 @@ role:parentServer:ADMIN ==> perm:asset:INSERT role:asset:OWNER ==> perm:asset:DELETE role:asset:ADMIN ==> perm:asset:UPDATE role:asset:TENANT ==> perm:asset:SELECT +role:global:ADMIN ==> perm:asset:INSERT ``` diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.md b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.md index cbbd80c0..b9a65745 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.md +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.md @@ -1,4 +1,4 @@ -### rbac asset inOtherCases +### rbac asset This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually. @@ -15,6 +15,7 @@ subgraph asset["`**asset**`"] role:asset:OWNER[[asset:OWNER]] role:asset:ADMIN[[asset:ADMIN]] + role:asset:AGENT[[asset:AGENT]] role:asset:TENANT[[asset:TENANT]] end @@ -42,48 +43,20 @@ subgraph bookingItem["`**bookingItem**`"] end end -subgraph bookingItem.debitorRel["`**bookingItem.debitorRel**`"] - direction TB - style bookingItem.debitorRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px - - subgraph bookingItem.debitorRel:roles[ ] - style bookingItem.debitorRel:roles fill:#99bcdb,stroke:white - - role:bookingItem.debitorRel:OWNER[[bookingItem.debitorRel:OWNER]] - role:bookingItem.debitorRel:ADMIN[[bookingItem.debitorRel:ADMIN]] - role:bookingItem.debitorRel:AGENT[[bookingItem.debitorRel:AGENT]] - role:bookingItem.debitorRel:TENANT[[bookingItem.debitorRel:TENANT]] - end -end - -subgraph parentServer["`**parentServer**`"] - direction TB - style parentServer fill:#99bcdb,stroke:#274d6e,stroke-width:8px - - subgraph parentServer:roles[ ] - style parentServer:roles fill:#99bcdb,stroke:white - - role:parentServer:ADMIN[[parentServer:ADMIN]] - end -end - %% granting roles to roles -role:global:ADMIN -.-> role:bookingItem.debitorRel:OWNER -role:bookingItem.debitorRel:OWNER -.-> role:bookingItem.debitorRel:ADMIN -role:bookingItem.debitorRel:ADMIN -.-> role:bookingItem.debitorRel:AGENT -role:bookingItem.debitorRel:AGENT -.-> role:bookingItem.debitorRel:TENANT -role:bookingItem.debitorRel:AGENT -.-> role:bookingItem:OWNER role:bookingItem:OWNER -.-> role:bookingItem:ADMIN -role:bookingItem.debitorRel:AGENT -.-> role:bookingItem:ADMIN role:bookingItem:ADMIN -.-> role:bookingItem:AGENT role:bookingItem:AGENT -.-> role:bookingItem:TENANT -role:bookingItem:TENANT -.-> role:bookingItem.debitorRel:TENANT role:bookingItem:ADMIN ==> role:asset:OWNER role:asset:OWNER ==> role:asset:ADMIN -role:asset:ADMIN ==> role:asset:TENANT +role:bookingItem:AGENT ==> role:asset:ADMIN +role:asset:ADMIN ==> role:asset:AGENT +role:asset:AGENT ==> role:asset:TENANT role:asset:TENANT ==> role:bookingItem:TENANT %% granting permissions to roles +role:global:ADMIN ==> perm:asset:INSERT +role:bookingItem:AGENT ==> perm:asset:INSERT role:asset:OWNER ==> perm:asset:DELETE role:asset:ADMIN ==> perm:asset:UPDATE role:asset:TENANT ==> perm:asset:SELECT diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql index 2495f1ea..ae6c51c7 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql @@ -30,41 +30,47 @@ create or replace procedure buildRbacSystemForHsHostingAsset( language plpgsql as $$ declare - newParentServer hs_hosting_asset; newBookingItem hs_booking_item; + newParentAsset hs_hosting_asset; begin call enterTriggerForObjectUuid(NEW.uuid); - SELECT * FROM hs_hosting_asset WHERE uuid = NEW.parentAssetUuid INTO newParentServer; - SELECT * FROM hs_booking_item WHERE uuid = NEW.bookingItemUuid INTO newBookingItem; + SELECT * FROM hs_hosting_asset WHERE uuid = NEW.parentAssetUuid INTO newParentAsset; + perform createRoleWithGrants( hsHostingAssetOWNER(NEW), permissions => array['DELETE'], - incomingSuperRoles => array[hsBookingItemADMIN(newBookingItem)] + incomingSuperRoles => array[ + hsBookingItemADMIN(newBookingItem), + hsHostingAssetADMIN(newParentAsset)] ); perform createRoleWithGrants( hsHostingAssetADMIN(NEW), permissions => array['UPDATE'], - incomingSuperRoles => array[hsHostingAssetOWNER(NEW)] + incomingSuperRoles => array[ + hsBookingItemAGENT(newBookingItem), + hsHostingAssetAGENT(newParentAsset), + hsHostingAssetOWNER(NEW)] + ); + + perform createRoleWithGrants( + hsHostingAssetAGENT(NEW), + incomingSuperRoles => array[hsHostingAssetADMIN(NEW)] ); perform createRoleWithGrants( hsHostingAssetTENANT(NEW), permissions => array['SELECT'], - incomingSuperRoles => array[hsHostingAssetADMIN(NEW)], - outgoingSubRoles => array[hsBookingItemTENANT(newBookingItem)] + incomingSuperRoles => array[hsHostingAssetAGENT(NEW)], + outgoingSubRoles => array[ + hsBookingItemTENANT(newBookingItem), + hsHostingAssetTENANT(newParentAsset)] ); - IF NEW.type = 'CLOUD_SERVER' THEN - ELSIF NEW.type = 'MANAGED_SERVER' THEN - ELSIF NEW.type = 'MANAGED_WEBSPACE' THEN - ELSE - END IF; - call leaveTriggerForObjectUuid(NEW.uuid); end; $$; @@ -92,6 +98,49 @@ execute procedure insertTriggerForHsHostingAsset_tf(); --changeset hs-hosting-asset-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--// -- ---------------------------------------------------------------------------- +-- granting INSERT permission to global ---------------------------- + +/* + Grants INSERT INTO hs_hosting_asset permissions to specified role of pre-existing global rows. + */ +do language plpgsql $$ + declare + row global; + begin + call defineContext('create INSERT INTO hs_hosting_asset permissions for pre-exising global rows'); + + FOR row IN SELECT * FROM global + -- unconditional for all rows in that table + LOOP + call grantPermissionToRole( + createPermission(row.uuid, 'INSERT', 'hs_hosting_asset'), + globalADMIN()); + END LOOP; + end; +$$; + +/** + Grants hs_hosting_asset INSERT permission to specified role of new global rows. +*/ +create or replace function new_hs_hosting_asset_grants_insert_to_global_tf() + returns trigger + language plpgsql + strict as $$ +begin + -- unconditional for all rows in that table + call grantPermissionToRole( + createPermission(NEW.uuid, 'INSERT', 'hs_hosting_asset'), + globalADMIN()); + -- end. + return NEW; +end; $$; + +-- z_... is to put it at the end of after insert triggers, to make sure the roles exist +create trigger z_new_hs_hosting_asset_grants_insert_to_global_tg + after insert on global + for each row +execute procedure new_hs_hosting_asset_grants_insert_to_global_tf(); + -- granting INSERT permission to hs_booking_item ---------------------------- /* @@ -176,17 +225,21 @@ create or replace function hs_hosting_asset_insert_permission_check_tf() declare superObjectUuid uuid; begin + -- check INSERT INSERT if global ADMIN + if isGlobalAdmin() then + return NEW; + end if; -- check INSERT permission via direct foreign key: NEW.bookingItemUuid - if NEW.type in ('MANAGED_SERVER', 'CLOUD_SERVER', 'MANAGED_WEBSPACE') and hasInsertPermission(NEW.bookingItemUuid, 'hs_hosting_asset') then + if hasInsertPermission(NEW.bookingItemUuid, 'hs_hosting_asset') then return NEW; end if; -- check INSERT permission via direct foreign key: NEW.parentAssetUuid - if NEW.type in ('MANAGED_WEBSPACE') and hasInsertPermission(NEW.parentAssetUuid, 'hs_hosting_asset') then + if hasInsertPermission(NEW.parentAssetUuid, 'hs_hosting_asset') then return NEW; end if; - raise exception '[403] insert into hs_hosting_asset not allowed for current subjects % (%)', - currentSubjects(), currentSubjectsUuids(); + raise exception '[403] insert into hs_hosting_asset values(%) not allowed for current subjects % (%)', + NEW, currentSubjects(), currentSubjectsUuids(); end; $$; create trigger hs_hosting_asset_insert_permission_check_tg @@ -200,11 +253,9 @@ create trigger hs_hosting_asset_insert_permission_check_tg --changeset hs-hosting-asset-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityViewFromQuery('hs_hosting_asset', +call generateRbacIdentityViewFromProjection('hs_hosting_asset', $idName$ - SELECT asset.uuid as uuid, bookingItemIV.idName || '-' || cleanIdentifier(asset.identifier) as idName - FROM hs_hosting_asset asset - JOIN hs_booking_item_iv bookingItemIV ON bookingItemIV.uuid = asset.bookingItemUuid + identifier $idName$); --// diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql index e8bcbc05..737b691a 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql @@ -8,46 +8,49 @@ /* Creates a single hs_hosting_asset test record. */ -create or replace procedure createHsHostingAssetTestData( - givenPartnerNumber numeric, - givenDebitorSuffix char(2), - givenWebspacePrefix char(3) - ) +create or replace procedure createHsHostingAssetTestData(givenProjectCaption varchar) language plpgsql as $$ declare currentTask varchar; + relatedProject hs_booking_project; relatedDebitor hs_office_debitor; relatedPrivateCloudBookingItem hs_booking_item; relatedManagedServerBookingItem hs_booking_item; managedServerUuid uuid; begin - currentTask := 'creating hosting-asset test-data ' || givenPartnerNumber::text || givenDebitorSuffix; + currentTask := 'creating hosting-asset test-data ' || givenProjectCaption; call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN'); execute format('set local hsadminng.currentTask to %L', currentTask); + select project.* into relatedProject + from hs_booking_project project + where project.caption = givenProjectCaption; + assert relatedProject.uuid is not null, 'relatedProject for "' || givenProjectCaption || '" must not be null'; + select debitor.* into relatedDebitor - from hs_office_debitor debitor - join hs_office_relation debitorRel on debitorRel.uuid = debitor.debitorRelUuid - join hs_office_relation partnerRel on partnerRel.holderUuid = debitorRel.anchorUuid - join hs_office_partner partner on partner.partnerRelUuid = partnerRel.uuid - where partner.partnerNumber = givenPartnerNumber and debitor.debitorNumberSuffix = givenDebitorSuffix; - select item.uuid into relatedPrivateCloudBookingItem + from hs_office_debitor debitor + where debitor.uuid = relatedProject.debitorUuid; + assert relatedDebitor.uuid is not null, 'relatedDebitor for "' || givenProjectCaption || '" must not be null'; + + select item.* into relatedPrivateCloudBookingItem from hs_booking_item item - where item.debitoruuid = relatedDebitor.uuid + where item.projectUuid = relatedProject.uuid and item.type = 'PRIVATE_CLOUD'; - select item.uuid into relatedManagedServerBookingItem + assert relatedPrivateCloudBookingItem.uuid is not null, 'relatedPrivateCloudBookingItem for "' || givenProjectCaption|| '" must not be null'; + + select item.* into relatedManagedServerBookingItem from hs_booking_item item - where item.debitoruuid = relatedDebitor.uuid + where item.projectUuid = relatedProject.uuid and item.type = 'MANAGED_SERVER'; + assert relatedManagedServerBookingItem.uuid is not null, 'relatedManagedServerBookingItem for "' || givenProjectCaption|| '" must not be null'; + select uuid_generate_v4() into managedServerUuid; - raise notice 'creating test hosting-asset: %', givenPartnerNumber::text || givenDebitorSuffix::text; - raise notice '- using debitor (%): %', relatedDebitor.uuid, relatedDebitor; insert into hs_hosting_asset - (uuid, bookingitemuuid, type, parentAssetUuid, identifier, caption, config) - values (managedServerUuid, relatedPrivateCloudBookingItem.uuid, 'MANAGED_SERVER', null, 'vm10' || givenDebitorSuffix, 'some ManagedServer', '{ "CPU": 2, "SDD": 512, "extra": 42 }'::jsonb), - (uuid_generate_v4(), relatedPrivateCloudBookingItem.uuid, 'CLOUD_SERVER', null, 'vm20' || givenDebitorSuffix, 'another CloudServer', '{ "CPU": 2, "HDD": 1024, "extra": 42 }'::jsonb), - (uuid_generate_v4(), relatedManagedServerBookingItem.uuid, 'MANAGED_WEBSPACE', managedServerUuid, givenWebspacePrefix || '01', 'some Webspace', '{ "RAM": 1, "SDD": 512, "HDD": 2048, "extra": 42 }'::jsonb); + (uuid, bookingitemuuid, type, parentAssetUuid, identifier, caption, config) + values (managedServerUuid, relatedPrivateCloudBookingItem.uuid, 'MANAGED_SERVER', null, 'vm10' || relatedDebitor.debitorNumberSuffix, 'some ManagedServer', '{ "CPU": 2, "SDD": 512, "extra": 42 }'::jsonb), + (uuid_generate_v4(), relatedPrivateCloudBookingItem.uuid, 'CLOUD_SERVER', null, 'vm20' || relatedDebitor.debitorNumberSuffix, 'another CloudServer', '{ "CPU": 2, "HDD": 1024, "extra": 42 }'::jsonb), + (uuid_generate_v4(), relatedManagedServerBookingItem.uuid, 'MANAGED_WEBSPACE', managedServerUuid, relatedDebitor.defaultPrefix || '01', 'some Webspace', '{ "RAM": 1, "SDD": 512, "HDD": 2048, "extra": 42 }'::jsonb); end; $$; --// @@ -58,9 +61,9 @@ end; $$; do language plpgsql $$ begin - call createHsHostingAssetTestData(10001, '11', 'aaa'); - call createHsHostingAssetTestData(10002, '12', 'bbb'); - call createHsHostingAssetTestData(10003, '13', 'ccc'); + call createHsHostingAssetTestData('D-1000111 default project'); + call createHsHostingAssetTestData('D-1000212 default project'); + call createHsHostingAssetTestData('D-1000313 default project'); end; $$; --// diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 90cbdcc2..aebf347d 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -130,11 +130,17 @@ databaseChangeLog: - include: file: db/changelog/5-hs-office/512-coopassets/5128-hs-office-coopassets-test-data.sql - include: - file: db/changelog/6-hs-booking/601-booking-item/6010-hs-booking-item.sql + file: db/changelog/6-hs-booking/610-booking-project/6100-hs-booking-project.sql - include: - file: db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql + file: db/changelog/6-hs-booking/610-booking-project/6103-hs-booking-project-rbac.sql - include: - file: db/changelog/6-hs-booking/601-booking-item/6018-hs-booking-item-test-data.sql + file: db/changelog/6-hs-booking/610-booking-project/6108-hs-booking-project-test-data.sql + - include: + file: db/changelog/6-hs-booking/620-booking-item/6200-hs-booking-item.sql + - include: + file: db/changelog/6-hs-booking/620-booking-item/6203-hs-booking-item-rbac.sql + - include: + file: db/changelog/6-hs-booking/620-booking-item/6208-hs-booking-item-test-data.sql - include: file: db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql - include: diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index 0cb1a086..2c2f9f3d 100644 --- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java +++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java @@ -39,6 +39,7 @@ public class ArchitectureTest { "..context", "..generated..", "..persistence..", + "..validation..", "..hs.office.bankaccount", "..hs.office.contact", "..hs.office.coopassets", @@ -50,9 +51,11 @@ public class ArchitectureTest { "..hs.office.person", "..hs.office.relation", "..hs.office.sepamandate", + "..hs.booking.project", "..hs.booking.item", + "..hs.booking.item.validators", "..hs.hosting.asset", - "..hs.hosting.asset.validator", + "..hs.hosting.asset.validators", "..errors", "..mapper", "..ping", diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index 0a92ff3f..7f385824 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java @@ -4,6 +4,9 @@ import io.hypersistence.utils.hibernate.type.range.Range; import io.restassured.RestAssured; import io.restassured.http.ContentType; import net.hostsharing.hsadminng.HsadminNgApplication; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository; +import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; @@ -17,10 +20,12 @@ import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import java.time.LocalDate; +import java.util.List; import java.util.Map; import java.util.UUID; import static java.util.Map.entry; +import static java.util.Optional.ofNullable; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE; import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; import static org.assertj.core.api.Assertions.assertThat; @@ -39,6 +44,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup @Autowired HsBookingItemRepository bookingItemRepo; + @Autowired + HsBookingProjectRepository projectRepo; + @Autowired HsOfficeDebitorRepository debitorRepo; @@ -56,22 +64,38 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup // given context("superuser-alex@hostsharing.net"); - final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(1000111).get(0); + final var givenProject = debitorRepo.findDebitorByDebitorNumber(1000111).stream() + .map(d -> projectRepo.findAllByDebitorUuid(d.getUuid())) + .flatMap(List::stream) + .findFirst() + .orElseThrow(); RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") .port(port) .when() - .get("http://localhost/api/hs/booking/items?debitorUuid=" + givenDebitor.getUuid()) + .get("http://localhost/api/hs/booking/items?projectUuid=" + givenProject.getUuid()) .then().log().all().assertThat() .statusCode(200) .contentType("application/json") .body("", lenientlyEquals(""" [ + { + "type": "MANAGED_WEBSPACE", + "caption": "some ManagedWebspace", + "validFrom": "2022-10-01", + "validTo": null, + "resources": { + "SDD": 512, + "Multi": 4, + "Daemons": 2, + "Traffic": 12 + } + }, { "type": "MANAGED_SERVER", - "caption": "some ManagedServer", + "caption": "separate ManagedServer", "validFrom": "2022-10-01", "validTo": null, "resources": { @@ -81,18 +105,6 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "Traffic": 42 } }, - { - "type": "CLOUD_SERVER", - "caption": "some CloudServer", - "validFrom": "2023-01-15", - "validTo": "2024-04-14", - "resources": { - "HDD": 1024, - "RAM": 4, - "CPUs": 2, - "Traffic": 42 - } - }, { "type": "PRIVATE_CLOUD", "caption": "some PrivateCloud", @@ -118,7 +130,11 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup void globalAdmin_canAddBookingItem() { context.define("superuser-alex@hostsharing.net"); - final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(1000111).get(0); + final var givenProject = debitorRepo.findDebitorByDebitorNumber(1000111).stream() + .map(d -> projectRepo.findAllByDebitorUuid(d.getUuid())) + .flatMap(List::stream) + .findFirst() + .orElseThrow(); final var location = RestAssured // @formatter:off .given() @@ -126,13 +142,13 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .contentType(ContentType.JSON) .body(""" { - "debitorUuid": "%s", + "projectUuid": "%s", "type": "MANAGED_SERVER", "caption": "some new booking", "resources": { "CPUs": 12, "RAM": 4, "SSD": 100, "Traffic": 250 }, "validFrom": "2022-10-13" } - """.formatted(givenDebitor.getUuid())) + """.formatted(givenProject.getUuid())) .port(port) .when() .post("http://localhost/api/hs/booking/items") @@ -165,8 +181,8 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup void globalAdmin_canGetArbitraryBookingItem() { context.define("superuser-alex@hostsharing.net"); final var givenBookingItemUuid = bookingItemRepo.findAll().stream() - .filter(bi -> bi.getDebitor().getDebitorNumber() == 1000111) - .filter(item -> item.getCaption().equals("some CloudServer")) + .filter(bi -> belongsToDebitorNumber(bi, 1000111)) + .filter(item -> item.getCaption().equals("some ManagedWebspace")) .findAny().orElseThrow().getUuid(); RestAssured // @formatter:off @@ -180,14 +196,15 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .contentType("application/json") .body("", lenientlyEquals(""" { - "caption": "some CloudServer", - "validFrom": "2023-01-15", - "validTo": "2024-04-14", - "resources": { - "HDD": 1024, - "RAM": 4, - "CPUs": 2, - "Traffic": 42 + "type": "MANAGED_WEBSPACE", + "caption": "some ManagedWebspace", + "validFrom": "2022-10-01", + "validTo": null, + "resources": { + "SDD": 512, + "Multi": 4, + "Daemons": 2, + "Traffic": 12 } } """)); // @formatter:on @@ -197,7 +214,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup void normalUser_canNotGetUnrelatedBookingItem() { context.define("superuser-alex@hostsharing.net"); final var givenBookingItemUuid = bookingItemRepo.findAll().stream() - .filter(bi -> bi.getDebitor().getDebitorNumber() == 1000212) + .filter(bi -> belongsToDebitorNumber(bi, 1000212)) .map(HsBookingItemEntity::getUuid) .findAny().orElseThrow(); @@ -215,8 +232,8 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup void debitorAgentUser_canGetRelatedBookingItem() { context.define("superuser-alex@hostsharing.net"); final var givenBookingItemUuid = bookingItemRepo.findAll().stream() - .filter(bi -> bi.getDebitor().getDebitorNumber() == 1000313) - .filter(item -> item.getCaption().equals("some CloudServer")) + .filter(bi -> belongsToDebitorNumber(bi, 1000313)) + .filter(item -> item.getCaption().equals("separate ManagedServer")) .findAny().orElseThrow().getUuid(); RestAssured // @formatter:off @@ -230,18 +247,28 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .contentType("application/json") .body("", lenientlyEquals(""" { - "caption": "some CloudServer", - "validFrom": "2023-01-15", - "validTo": "2024-04-14", + "type": "MANAGED_SERVER", + "caption": "separate ManagedServer", + "validFrom": "2022-10-01", + "validTo": null, "resources": { - "HDD": 1024, - "RAM": 4, + "RAM": 8, + "SDD": 512, "CPUs": 2, "Traffic": 42 } } """)); // @formatter:on } + + private static boolean belongsToDebitorNumber(final HsBookingItemEntity bi, final int i) { + return ofNullable(bi) + .map(HsBookingItemEntity::getProject) + .map(HsBookingProjectEntity::getDebitor) + .map(HsOfficeDebitorEntity::getDebitorNumber) + .filter(debitorNumber -> debitorNumber == i) + .isPresent(); + } } @Nested @@ -290,7 +317,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup context.define("superuser-alex@hostsharing.net"); assertThat(bookingItemRepo.findByUuid(givenBookingItem.getUuid())).isPresent().get() .matches(mandate -> { - assertThat(mandate.getDebitor().toString()).isEqualTo("debitor(D-1000111: rel(anchor='LP First GmbH', type='DEBITOR', holder='LP First GmbH'), fir)"); + assertThat(mandate.getProject().getDebitor().toString()).isEqualTo("debitor(D-1000111: rel(anchor='LP First GmbH', type='DEBITOR', holder='LP First GmbH'), fir)"); assertThat(mandate.getValidFrom()).isEqualTo("2022-11-01"); assertThat(mandate.getValidTo()).isEqualTo("2022-12-31"); return true; @@ -345,10 +372,13 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup final HsBookingItemType hsBookingItemType, final Map.Entry... resources) { return jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net"); - final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(debitorNumber).get(0); + final var givenProject = debitorRepo.findDebitorByDebitorNumber(debitorNumber).stream() + .map(d -> projectRepo.findAllByDebitorUuid(d.getUuid())) + .flatMap(java.util.List::stream) + .findAny().orElseThrow(); final var newBookingItem = HsBookingItemEntity.builder() .uuid(UUID.randomUUID()) - .debitor(givenDebitor) + .project(givenProject) .type(hsBookingItemType) .caption("some test-booking") .resources(Map.ofEntries(resources)) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java index b7ff8ab4..7e312fbc 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityPatcherUnitTest.java @@ -17,7 +17,7 @@ import java.util.Map; import java.util.UUID; import java.util.stream.Stream; -import static net.hostsharing.hsadminng.hs.office.debitor.TestHsOfficeDebitor.TEST_DEBITOR; +import static net.hostsharing.hsadminng.hs.booking.project.TestHsBookingProject.TEST_PROJECT; import static net.hostsharing.hsadminng.mapper.PatchMap.entry; import static net.hostsharing.hsadminng.mapper.PatchMap.patchMap; import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; @@ -70,7 +70,7 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase< protected HsBookingItemEntity newInitialEntity() { final var entity = new HsBookingItemEntity(); entity.setUuid(INITIAL_BOOKING_ITEM_UUID); - entity.setDebitor(TEST_DEBITOR); + entity.setProject(TEST_PROJECT); entity.getResources().putAll(KeyValueMap.from(INITIAL_RESOURCES)); entity.setCaption(INITIAL_CAPTION); entity.setValidity(Range.closedInfinite(GIVEN_VALID_FROM)); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java index 72d373e0..f311bd09 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java @@ -6,7 +6,7 @@ import java.time.LocalDate; import java.util.Map; import static java.util.Map.entry; -import static net.hostsharing.hsadminng.hs.office.debitor.TestHsOfficeDebitor.TEST_DEBITOR; +import static net.hostsharing.hsadminng.hs.booking.project.TestHsBookingProject.TEST_PROJECT; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; import static org.assertj.core.api.Assertions.assertThat; @@ -15,7 +15,7 @@ class HsBookingItemEntityUnitTest { public static final LocalDate GIVEN_VALID_TO = LocalDate.parse("2030-12-31"); final HsBookingItemEntity givenBookingItem = HsBookingItemEntity.builder() - .debitor(TEST_DEBITOR) + .project(TEST_PROJECT) .type(HsBookingItemType.CLOUD_SERVER) .caption("some caption") .resources(Map.ofEntries( @@ -29,14 +29,14 @@ class HsBookingItemEntityUnitTest { void toStringContainsAllPropertiesAndResourcesSortedByKey() { final var result = givenBookingItem.toString(); - assertThat(result).isEqualTo("HsBookingItemEntity(D-1000100, CLOUD_SERVER, [2020-01-01,2031-01-01), some caption, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })"); + assertThat(result).isEqualTo("HsBookingItemEntity(D-1000100:test project, CLOUD_SERVER, [2020-01-01,2031-01-01), some caption, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })"); } @Test void toShortStringContainsOnlyMemberNumberAndCaption() { final var result = givenBookingItem.toShortString(); - assertThat(result).isEqualTo("D-1000100:some caption"); + assertThat(result).isEqualTo("D-1000100:test project:some caption"); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java index c76d30df..f4ac6fee 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java @@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.hs.booking.item; import io.hypersistence.utils.hibernate.type.range.Range; import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; @@ -40,6 +41,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup @Autowired HsBookingItemRepository bookingItemRepo; + @Autowired + HsBookingProjectRepository projectRepo; + @Autowired HsOfficeDebitorRepository debitorRepo; @@ -67,11 +71,12 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup context("superuser-alex@hostsharing.net"); final var count = bookingItemRepo.count(); final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("First").get(0); + final var givenProject = projectRepo.findAllByDebitorUuid(givenDebitor.getUuid()).get(0); // when final var result = attempt(em, () -> { final var newBookingItem = HsBookingItemEntity.builder() - .debitor(givenDebitor) + .project(givenProject) .type(HsBookingItemType.CLOUD_SERVER) .caption("some new booking item") .validity(Range.closedOpen( @@ -99,8 +104,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // when attempt(em, () -> { final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("First").get(0); + final var givenProject = projectRepo.findAllByDebitorUuid(givenDebitor.getUuid()).get(0); final var newBookingItem = HsBookingItemEntity.builder() - .debitor(givenDebitor) + .project(givenProject) .type(MANAGED_WEBSPACE) .caption("some new booking item") .validity(Range.closedOpen( @@ -113,35 +119,34 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup final var all = rawRoleRepo.findAll(); assertThat(distinctRoleNamesOf(all)).containsExactlyInAnyOrder(Array.from( initialRoleNames, - "hs_booking_item#D-1000111-somenewbookingitem:ADMIN", - "hs_booking_item#D-1000111-somenewbookingitem:AGENT", - "hs_booking_item#D-1000111-somenewbookingitem:OWNER", - "hs_booking_item#D-1000111-somenewbookingitem:TENANT")); + "hs_booking_item#somenewbookingitem:ADMIN", + "hs_booking_item#somenewbookingitem:AGENT", + "hs_booking_item#somenewbookingitem:OWNER", + "hs_booking_item#somenewbookingitem:TENANT")); assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())) .map(s -> s.replace("hs_office_", "")) .containsExactlyInAnyOrder(fromFormatted( initialGrantNames, // global-admin - "{ grant perm:hs_booking_item#D-1000111-somenewbookingitem:DELETE to role:global#global:ADMIN by system and assume }", + "{ grant perm:hs_booking_item#somenewbookingitem:INSERT>hs_booking_item to role:hs_booking_item#somenewbookingitem:ADMIN by system and assume }", + "{ grant perm:hs_booking_item#somenewbookingitem:DELETE to role:global#global:ADMIN by system and assume }", // owner - "{ grant role:hs_booking_item#D-1000111-somenewbookingitem:OWNER to role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:AGENT by system and assume }", + "{ grant role:hs_booking_item#somenewbookingitem:OWNER to role:hs_booking_project#D-1000111-D-1000111defaultproject:AGENT by system and assume }", // admin - "{ grant perm:hs_booking_item#D-1000111-somenewbookingitem:UPDATE to role:hs_booking_item#D-1000111-somenewbookingitem:ADMIN by system and assume }", - "{ grant role:hs_booking_item#D-1000111-somenewbookingitem:ADMIN to role:hs_booking_item#D-1000111-somenewbookingitem:OWNER by system and assume }", - "{ grant perm:hs_booking_item#D-1000111-somenewbookingitem:INSERT>hs_hosting_asset to role:hs_booking_item#D-1000111-somenewbookingitem:AGENT by system and assume }", + "{ grant perm:hs_booking_item#somenewbookingitem:UPDATE to role:hs_booking_item#somenewbookingitem:ADMIN by system and assume }", + "{ grant role:hs_booking_item#somenewbookingitem:ADMIN to role:hs_booking_item#somenewbookingitem:OWNER by system and assume }", + "{ grant perm:hs_booking_item#somenewbookingitem:INSERT>hs_hosting_asset to role:hs_booking_item#somenewbookingitem:AGENT by system and assume }", // agent - "{ grant role:hs_booking_item#D-1000111-somenewbookingitem:ADMIN to role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:AGENT by system and assume }", - "{ grant role:hs_booking_item#D-1000111-somenewbookingitem:AGENT to role:hs_booking_item#D-1000111-somenewbookingitem:ADMIN by system and assume }", + "{ grant role:hs_booking_item#somenewbookingitem:AGENT to role:hs_booking_item#somenewbookingitem:ADMIN by system and assume }", // tenant - "{ grant role:hs_booking_item#D-1000111-somenewbookingitem:TENANT to role:hs_booking_item#D-1000111-somenewbookingitem:AGENT by system and assume }", - "{ grant perm:hs_booking_item#D-1000111-somenewbookingitem:SELECT to role:hs_booking_item#D-1000111-somenewbookingitem:TENANT by system and assume }", - "{ grant role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:TENANT to role:hs_booking_item#D-1000111-somenewbookingitem:TENANT by system and assume }", - + "{ grant role:hs_booking_item#somenewbookingitem:TENANT to role:hs_booking_item#somenewbookingitem:AGENT by system and assume }", + "{ grant perm:hs_booking_item#somenewbookingitem:SELECT to role:hs_booking_item#somenewbookingitem:TENANT by system and assume }", + "{ grant role:hs_booking_project#D-1000111-D-1000111defaultproject:TENANT to role:hs_booking_item#somenewbookingitem:TENANT by system and assume }", null)); } @@ -158,35 +163,40 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup public void globalAdmin_withoutAssumedRole_canViewAllBookingItemsOfArbitraryDebitor() { // given context("superuser-alex@hostsharing.net"); - final var debitorUuid = debitorRepo.findDebitorByDebitorNumber(1000212).stream() + final var projectUuid = debitorRepo.findDebitorByDebitorNumber(1000212).stream() + .map(d -> projectRepo.findAllByDebitorUuid(d.getUuid())) + .flatMap(List::stream) .findAny().orElseThrow().getUuid(); // when - final var result = bookingItemRepo.findAllByDebitorUuid(debitorUuid); + final var result = bookingItemRepo.findAllByProjectUuid(projectUuid); // then allTheseBookingItemsAreReturned( result, - "HsBookingItemEntity(D-1000212, MANAGED_SERVER, [2022-10-01,), some ManagedServer, { CPUs: 2, RAM: 8, SDD: 512, Traffic: 42 })", - "HsBookingItemEntity(D-1000212, CLOUD_SERVER, [2023-01-15,2024-04-15), some CloudServer, { CPUs: 2, HDD: 1024, RAM: 4, Traffic: 42 })", - "HsBookingItemEntity(D-1000212, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10240, SDD: 10240, Traffic: 42 })"); + "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SDD: 512, Traffic: 42 })", + "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_WEBSPACE, [2022-10-01,), some ManagedWebspace, { Daemons: 2, Multi: 4, SDD: 512, Traffic: 12 })", + "HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10240, SDD: 10240, Traffic: 42 })"); } @Test public void normalUser_canViewOnlyRelatedBookingItems() { // given: context("person-FirbySusan@example.com"); - final var debitorUuid = debitorRepo.findDebitorByDebitorNumber(1000111).stream().findAny().orElseThrow().getUuid(); + final var projectUuid = debitorRepo.findDebitorByDebitorNumber(1000111).stream() + .map(d -> projectRepo.findAllByDebitorUuid(d.getUuid())) + .flatMap(List::stream) + .findAny().orElseThrow().getUuid(); // when: - final var result = bookingItemRepo.findAllByDebitorUuid(debitorUuid); + final var result = bookingItemRepo.findAllByProjectUuid(projectUuid); // then: exactlyTheseBookingItemsAreReturned( result, - "HsBookingItemEntity(D-1000111, MANAGED_SERVER, [2022-10-01,), some ManagedServer, { CPUs: 2, RAM: 8, SDD: 512, Traffic: 42 })", - "HsBookingItemEntity(D-1000111, CLOUD_SERVER, [2023-01-15,2024-04-15), some CloudServer, { CPUs: 2, HDD: 1024, RAM: 4, Traffic: 42 })", - "HsBookingItemEntity(D-1000111, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10240, SDD: 10240, Traffic: 42 })"); + "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SDD: 512, Traffic: 42 })", + "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_WEBSPACE, [2022-10-01,), some ManagedWebspace, { Daemons: 2, Multi: 4, SDD: 512, Traffic: 12 })", + "HsBookingItemEntity(D-1000111:D-1000111 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10240, SDD: 10240, Traffic: 42 })"); } } @@ -196,7 +206,7 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup @Test public void hostsharingAdmin_canUpdateArbitraryBookingItem() { // given - final var givenBookingItemUuid = givenSomeTemporaryBookingItem(1000111).getUuid(); + final var givenBookingItemUuid = givenSomeTemporaryBookingItem("D-1000111 default project").getUuid(); // when final var result = jpaAttempt.transacted(() -> { @@ -232,7 +242,7 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup public void globalAdmin_withoutAssumedRole_canDeleteAnyBookingItem() { // given context("superuser-alex@hostsharing.net", null); - final var givenBookingItem = givenSomeTemporaryBookingItem(1000111); + final var givenBookingItem = givenSomeTemporaryBookingItem("D-1000111 default project"); // when final var result = jpaAttempt.transacted(() -> { @@ -252,7 +262,7 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup public void nonGlobalAdmin_canNotDeleteTheirRelatedBookingItem() { // given context("superuser-alex@hostsharing.net", null); - final var givenBookingItem = givenSomeTemporaryBookingItem(1000111); + final var givenBookingItem = givenSomeTemporaryBookingItem("D-1000111 default project"); // when final var result = jpaAttempt.transacted(() -> { @@ -278,7 +288,7 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup context("superuser-alex@hostsharing.net"); final var initialRoleNames = Array.from(distinctRoleNamesOf(rawRoleRepo.findAll())); final var initialGrantNames = Array.from(distinctGrantDisplaysOf(rawGrantRepo.findAll())); - final var givenBookingItem = givenSomeTemporaryBookingItem(1000111); + final var givenBookingItem = givenSomeTemporaryBookingItem("D-1000111 default project"); // when final var result = jpaAttempt.transacted(() -> { @@ -313,12 +323,14 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup "[creating booking-item test-data 1000313, hs_booking_item, INSERT]"); } - private HsBookingItemEntity givenSomeTemporaryBookingItem(final int debitorNumber) { + private HsBookingItemEntity givenSomeTemporaryBookingItem(final String projectCaption) { return jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net"); - final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(debitorNumber).get(0); + final var givenProject = projectRepo.findAll().stream() + .filter(p -> p.getCaption().equals(projectCaption)) + .findAny().orElseThrow(); final var newBookingItem = HsBookingItemEntity.builder() - .debitor(givenDebitor) + .project(givenProject) .type(MANAGED_SERVER) .caption("some temp booking item") .validity(Range.closedOpen( diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/TestHsBookingItem.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/TestHsBookingItem.java index 1706cac4..00c0d706 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/TestHsBookingItem.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/TestHsBookingItem.java @@ -7,13 +7,13 @@ import java.time.LocalDate; import java.util.Map; import static java.util.Map.entry; -import static net.hostsharing.hsadminng.hs.office.debitor.TestHsOfficeDebitor.TEST_DEBITOR; +import static net.hostsharing.hsadminng.hs.booking.project.TestHsBookingProject.TEST_PROJECT; @UtilityClass public class TestHsBookingItem { public static final HsBookingItemEntity TEST_BOOKING_ITEM = HsBookingItemEntity.builder() - .debitor(TEST_DEBITOR) + .project(TEST_PROJECT) .caption("test booking item") .resources(Map.ofEntries( entry("someThing", 1), diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectControllerAcceptanceTest.java new file mode 100644 index 00000000..31bd8ba0 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectControllerAcceptanceTest.java @@ -0,0 +1,289 @@ +package net.hostsharing.hsadminng.hs.booking.project; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import net.hostsharing.hsadminng.HsadminNgApplication; +import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; +import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; +import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.util.Map; +import java.util.UUID; + +import static java.util.Map.entry; +import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.matchesRegex; + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = { HsadminNgApplication.class, JpaAttempt.class } +) +@Transactional +class HsBookingProjectControllerAcceptanceTest extends ContextBasedTestWithCleanup { + + @LocalServerPort + private Integer port; + + @Autowired + HsBookingProjectRepository bookingProjectRepo; + + @Autowired + HsBookingProjectRepository projectRepo; + + @Autowired + HsOfficeDebitorRepository debitorRepo; + + @Autowired + JpaAttempt jpaAttempt; + + @PersistenceContext + EntityManager em; + + @Nested + class ListBookingProjects { + + @Test + void globalAdmin_canViewAllBookingProjectsOfArbitraryDebitor() { + + // given + context("superuser-alex@hostsharing.net"); + final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(1000111).stream() + .findFirst() + .orElseThrow(); + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .port(port) + .when() + .get("http://localhost/api/hs/booking/projects?debitorUuid=" + givenDebitor.getUuid()) + .then().log().all().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + [ + { + "caption": "D-1000111 default project" + } + ] + """)); + // @formatter:on + } + } + + @Nested + class AddBookingProject { + + @Test + void globalAdmin_canAddBookingProject() { + + context.define("superuser-alex@hostsharing.net"); + final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(1000111).stream() + .findFirst() + .orElseThrow(); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "debitorUuid": "%s", + "caption": "some new project" + } + """.formatted(givenDebitor.getUuid())) + .port(port) + .when() + .post("http://localhost/api/hs/booking/projects") + .then().log().all().assertThat() + .statusCode(201) + .contentType(ContentType.JSON) + .body("", lenientlyEquals(""" + { + "caption": "some new project" + } + """)) + .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/booking/projects/[^/]*")) + .extract().header("Location"); // @formatter:on + + // finally, the new bookingProject can be accessed under the generated UUID + final var newUserUuid = UUID.fromString( + location.substring(location.lastIndexOf('/') + 1)); + assertThat(newUserUuid).isNotNull(); + } + } + + @Nested + class GetBookingProject { + + @Test + void globalAdmin_canGetArbitraryBookingProject() { + context.define("superuser-alex@hostsharing.net"); + final var givenBookingProjectUuid = bookingProjectRepo.findAll().stream() + .filter(project -> project.getDebitor().getDebitorNumber() == 1000111) + .findAny().orElseThrow().getUuid(); + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .port(port) + .when() + .get("http://localhost/api/hs/booking/projects/" + givenBookingProjectUuid) + .then().log().all().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + { + "caption": "D-1000111 default project" + } + """)); // @formatter:on + } + + @Test + void normalUser_canNotGetUnrelatedBookingProject() { + context.define("superuser-alex@hostsharing.net"); + final var givenBookingProjectUuid = bookingProjectRepo.findAll().stream() + .filter(project -> project.getDebitor().getDebitorNumber() == 1000212) + .map(HsBookingProjectEntity::getUuid) + .findAny().orElseThrow(); + + RestAssured // @formatter:off + .given() + .header("current-user", "selfregistered-user-drew@hostsharing.org") + .port(port) + .when() + .get("http://localhost/api/hs/booking/projects/" + givenBookingProjectUuid) + .then().log().body().assertThat() + .statusCode(404); // @formatter:on + } + + @Test + void debitorAgentUser_canGetRelatedBookingProject() { + context.define("superuser-alex@hostsharing.net"); + final var givenBookingProjectUuid = bookingProjectRepo.findAll().stream() + .filter(project -> project.getDebitor().getDebitorNumber() == 1000313) + .findAny().orElseThrow().getUuid(); + + RestAssured // @formatter:off + .given() + .header("current-user", "person-TuckerJack@example.com") + .port(port) + .when() + .get("http://localhost/api/hs/booking/projects/" + givenBookingProjectUuid) + .then().log().all().assertThat() + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + { + "caption": "D-1000313 default project" + } + """)); // @formatter:on + } + } + + @Nested + class PatchBookingProject { + + @Test + void globalAdmin_canPatchAllUpdatablePropertiesOfBookingProject() { + + final var givenBookingProject = givenSomeBookingProject(1000111, "some project"); + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "caption": "some project" + } + """) + .port(port) + .when() + .patch("http://localhost/api/hs/booking/projects/" + givenBookingProject.getUuid()) + .then().log().all().assertThat() + .statusCode(200) + .contentType(ContentType.JSON) + .body("", lenientlyEquals(""" + { + "caption": "some project" + } + """)); // @formatter:on + + // finally, the bookingProject is actually updated + context.define("superuser-alex@hostsharing.net"); + assertThat(bookingProjectRepo.findByUuid(givenBookingProject.getUuid())).isPresent().get() + .matches(mandate -> { + assertThat(mandate.getDebitor().toString()).isEqualTo("debitor(D-1000111: rel(anchor='LP First GmbH', type='DEBITOR', holder='LP First GmbH'), fir)"); + return true; + }); + } + } + + @Nested + class DeleteBookingProject { + + @Test + void globalAdmin_canDeleteArbitraryBookingProject() { + context.define("superuser-alex@hostsharing.net"); + final var givenBookingProject = givenSomeBookingProject(1000111, "some project"); + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .port(port) + .when() + .delete("http://localhost/api/hs/booking/projects/" + givenBookingProject.getUuid()) + .then().log().body().assertThat() + .statusCode(204); // @formatter:on + + // then the given bookingProject is gone + assertThat(bookingProjectRepo.findByUuid(givenBookingProject.getUuid())).isEmpty(); + } + + @Test + void normalUser_canNotDeleteUnrelatedBookingProject() { + context.define("superuser-alex@hostsharing.net"); + final var givenBookingProject = givenSomeBookingProject(1000111, "some project"); + + RestAssured // @formatter:off + .given() + .header("current-user", "selfregistered-user-drew@hostsharing.org") + .port(port) + .when() + .delete("http://localhost/api/hs/booking/projects/" + givenBookingProject.getUuid()) + .then().log().body().assertThat() + .statusCode(404); // @formatter:on + + // then the given bookingProject is still there + assertThat(bookingProjectRepo.findByUuid(givenBookingProject.getUuid())).isNotEmpty(); + } + } + + private HsBookingProjectEntity givenSomeBookingProject(final int debitorNumber, final String caption) { + return jpaAttempt.transacted(() -> { + context.define("superuser-alex@hostsharing.net"); + final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(debitorNumber).stream().findAny().orElseThrow(); + final var newBookingProject = HsBookingProjectEntity.builder() + .uuid(UUID.randomUUID()) + .debitor(givenDebitor) + .caption(caption) + .build(); + + return bookingProjectRepo.save(newBookingProject); + }).assertSuccessful().returnedValue(); + } + + private Map.Entry resource(final String key, final Object value) { + return entry(key, value); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntityPatcherUnitTest.java new file mode 100644 index 00000000..cb059fe2 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntityPatcherUnitTest.java @@ -0,0 +1,74 @@ +package net.hostsharing.hsadminng.hs.booking.project; + +import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectPatchResource; +import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; +import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import jakarta.persistence.EntityManager; +import java.util.UUID; +import java.util.stream.Stream; + +import static net.hostsharing.hsadminng.hs.office.debitor.TestHsOfficeDebitor.TEST_DEBITOR; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; + +@TestInstance(PER_CLASS) +@ExtendWith(MockitoExtension.class) +class HsBookingProjectEntityPatcherUnitTest extends PatchUnitTestBase< + HsBookingProjectPatchResource, + HsBookingProjectEntity + > { + + private static final UUID INITIAL_BOOKING_PROJECT_UUID = UUID.randomUUID(); + + private static final String INITIAL_CAPTION = "initial caption"; + private static final String PATCHED_CAPTION = "patched caption"; + + @Mock + private EntityManager em; + + @BeforeEach + void initMocks() { + lenient().when(em.getReference(eq(HsOfficeDebitorEntity.class), any())).thenAnswer(invocation -> + HsOfficeDebitorEntity.builder().uuid(invocation.getArgument(1)).build()); + lenient().when(em.getReference(eq(HsBookingProjectEntity.class), any())).thenAnswer(invocation -> + HsBookingProjectEntity.builder().uuid(invocation.getArgument(1)).build()); + } + + @Override + protected HsBookingProjectEntity newInitialEntity() { + final var entity = new HsBookingProjectEntity(); + entity.setUuid(INITIAL_BOOKING_PROJECT_UUID); + entity.setDebitor(TEST_DEBITOR); + entity.setCaption(INITIAL_CAPTION); + return entity; + } + + @Override + protected HsBookingProjectPatchResource newPatchResource() { + return new HsBookingProjectPatchResource(); + } + + @Override + protected HsBookingProjectEntityPatcher createPatcher(final HsBookingProjectEntity bookingProject) { + return new HsBookingProjectEntityPatcher(bookingProject); + } + + @Override + protected Stream propertyTestDescriptors() { + return Stream.of( + new JsonNullableProperty<>( + "caption", + HsBookingProjectPatchResource::setCaption, + PATCHED_CAPTION, + HsBookingProjectEntity::setCaption) + ); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntityUnitTest.java new file mode 100644 index 00000000..dd911a8a --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntityUnitTest.java @@ -0,0 +1,27 @@ +package net.hostsharing.hsadminng.hs.booking.project; + +import org.junit.jupiter.api.Test; + +import static net.hostsharing.hsadminng.hs.office.debitor.TestHsOfficeDebitor.TEST_DEBITOR; +import static org.assertj.core.api.Assertions.assertThat; + +class HsBookingProjectEntityUnitTest { + final HsBookingProjectEntity givenBookingProject = HsBookingProjectEntity.builder() + .debitor(TEST_DEBITOR) + .caption("some caption") + .build(); + + @Test + void toStringContainsAllPropertiesAndResourcesSortedByKey() { + final var result = givenBookingProject.toString(); + + assertThat(result).isEqualTo("HsBookingProjectEntity(D-1000100, some caption)"); + } + + @Test + void toShortStringContainsOnlyMemberNumberAndCaption() { + final var result = givenBookingProject.toShortString(); + + assertThat(result).isEqualTo("D-1000100:some caption"); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java new file mode 100644 index 00000000..edc4649a --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java @@ -0,0 +1,326 @@ +package net.hostsharing.hsadminng.hs.booking.project; + +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; +import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; +import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; +import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; +import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.orm.jpa.JpaSystemException; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.List; + +import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf; +import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf; +import static net.hostsharing.hsadminng.rbac.test.Array.fromFormatted; +import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt; +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import({ Context.class, JpaAttempt.class }) +class HsBookingProjectRepositoryIntegrationTest extends ContextBasedTestWithCleanup { + + @Autowired + HsBookingProjectRepository bookingProjectRepo; + + @Autowired + HsBookingProjectRepository projectRepo; + + @Autowired + HsOfficeDebitorRepository debitorRepo; + + @Autowired + RawRbacRoleRepository rawRoleRepo; + + @Autowired + RawRbacGrantRepository rawGrantRepo; + + @Autowired + JpaAttempt jpaAttempt; + + @PersistenceContext + EntityManager em; + + @MockBean + HttpServletRequest request; + + @Nested + class CreateBookingProject { + + @Test + public void testHostsharingAdmin_withoutAssumedRole_canCreateNewBookingProject() { + // given + context("superuser-alex@hostsharing.net"); + final var count = bookingProjectRepo.count(); + final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("First").get(0); + + // when + final var result = attempt(em, () -> { + final var newBookingProject = HsBookingProjectEntity.builder() + .debitor(givenDebitor) + .caption("some new booking project") + .build(); + return toCleanup(bookingProjectRepo.save(newBookingProject)); + }); + + // then + result.assertSuccessful(); + assertThat(result.returnedValue()).isNotNull().extracting(HsBookingProjectEntity::getUuid).isNotNull(); + assertThatBookingProjectIsPersisted(result.returnedValue()); + assertThat(bookingProjectRepo.count()).isEqualTo(count + 1); + } + + @Test + public void createsAndGrantsRoles() { + // given + context("superuser-alex@hostsharing.net"); + final var initialRoleNames = distinctRoleNamesOf(rawRoleRepo.findAll()); + final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()).stream() + .map(s -> s.replace("hs_office_", "")) + .toList(); + + // when + attempt(em, () -> { + final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("First").get(0); + final var newBookingProject = HsBookingProjectEntity.builder() + .debitor(givenDebitor) + .caption("some new booking project") + .build(); + return toCleanup(bookingProjectRepo.save(newBookingProject)); + }); + + // then + final var all = rawRoleRepo.findAll(); + assertThat(distinctRoleNamesOf(all)).containsExactlyInAnyOrder(Array.from( + initialRoleNames, + "hs_booking_project#D-1000111-somenewbookingproject:ADMIN", + "hs_booking_project#D-1000111-somenewbookingproject:AGENT", + "hs_booking_project#D-1000111-somenewbookingproject:OWNER", + "hs_booking_project#D-1000111-somenewbookingproject:TENANT")); + assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())) + .map(s -> s.replace("hs_office_", "")) + .containsExactlyInAnyOrder(fromFormatted( + initialGrantNames, + + // global-admin + "{ grant perm:hs_booking_project#D-1000111-somenewbookingproject:DELETE to role:global#global:ADMIN by system and assume }", + + // owner + "{ grant role:hs_booking_project#D-1000111-somenewbookingproject:ADMIN to role:hs_booking_project#D-1000111-somenewbookingproject:OWNER by system and assume }", + + // admin + "{ grant role:hs_booking_project#D-1000111-somenewbookingproject:AGENT to role:hs_booking_project#D-1000111-somenewbookingproject:ADMIN by system and assume }", + "{ grant perm:hs_booking_project#D-1000111-somenewbookingproject:UPDATE to role:hs_booking_project#D-1000111-somenewbookingproject:ADMIN by system and assume }", + "{ grant perm:hs_booking_project#D-1000111-somenewbookingproject:INSERT>hs_booking_item to role:hs_booking_project#D-1000111-somenewbookingproject:ADMIN by system and assume }", + + // agent + "{ grant role:hs_booking_project#D-1000111-somenewbookingproject:OWNER to role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:AGENT by system and assume }", + "{ grant role:hs_booking_project#D-1000111-somenewbookingproject:TENANT to role:hs_booking_project#D-1000111-somenewbookingproject:AGENT by system and assume }", + + // tenant + "{ grant role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:TENANT to role:hs_booking_project#D-1000111-somenewbookingproject:TENANT by system and assume }", + "{ grant perm:hs_booking_project#D-1000111-somenewbookingproject:SELECT to role:hs_booking_project#D-1000111-somenewbookingproject:TENANT by system and assume }", + + null)); + } + + private void assertThatBookingProjectIsPersisted(final HsBookingProjectEntity saved) { + final var found = bookingProjectRepo.findByUuid(saved.getUuid()); + assertThat(found).isNotEmpty().map(HsBookingProjectEntity::toString).get().isEqualTo(saved.toString()); + } + } + + @Nested + class FindByDebitorUuid { + + @Test + public void globalAdmin_withoutAssumedRole_canViewAllBookingProjectsOfArbitraryDebitor() { + // given + context("superuser-alex@hostsharing.net"); + final var debitorUuid = debitorRepo.findDebitorByDebitorNumber(1000212).stream() + .findAny().orElseThrow().getUuid(); + + // when + final var result = bookingProjectRepo.findAllByDebitorUuid(debitorUuid); + + // then + allTheseBookingProjectsAreReturned( + result, + "HsBookingProjectEntity(D-1000212, D-1000212 default project)"); + } + + @Test + public void normalUser_canViewOnlyRelatedBookingProjects() { + // given: + context("person-FirbySusan@example.com"); + final var debitorUuid = debitorRepo.findDebitorByDebitorNumber(1000111).stream() + .findAny().orElseThrow().getUuid(); + + // when: + final var result = bookingProjectRepo.findAllByDebitorUuid(debitorUuid); + + // then: + exactlyTheseBookingProjectsAreReturned( + result, + "HsBookingProjectEntity(D-1000111, D-1000111 default project)"); + } + } + + @Nested + class UpdateBookingProject { + + @Test + public void hostsharingAdmin_canUpdateArbitraryBookingProject() { + // given + final var givenBookingProjectUuid = givenSomeTemporaryBookingProject(1000111).getUuid(); + + // when + final var result = jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net"); + final var foundBookingProject = em.find(HsBookingProjectEntity.class, givenBookingProjectUuid); + return toCleanup(bookingProjectRepo.save(foundBookingProject)); + }); + + // then + result.assertSuccessful(); + jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net"); + assertThatBookingProjectActuallyInDatabase(result.returnedValue()); + }).assertSuccessful(); + } + + private void assertThatBookingProjectActuallyInDatabase(final HsBookingProjectEntity saved) { + final var found = bookingProjectRepo.findByUuid(saved.getUuid()); + assertThat(found).isNotEmpty().get().isNotSameAs(saved) + .extracting(Object::toString).isEqualTo(saved.toString()); + } + } + + @Nested + class DeleteByUuid { + + @Test + public void globalAdmin_withoutAssumedRole_canDeleteAnyBookingProject() { + // given + context("superuser-alex@hostsharing.net", null); + final var givenBookingProject = givenSomeTemporaryBookingProject(1000111); + + // when + final var result = jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net"); + bookingProjectRepo.deleteByUuid(givenBookingProject.getUuid()); + }); + + // then + result.assertSuccessful(); + assertThat(jpaAttempt.transacted(() -> { + context("superuser-fran@hostsharing.net", null); + return bookingProjectRepo.findByUuid(givenBookingProject.getUuid()); + }).assertSuccessful().returnedValue()).isEmpty(); + } + + @Test + public void nonGlobalAdmin_canNotDeleteTheirRelatedBookingProject() { + // given + context("superuser-alex@hostsharing.net", null); + final var givenBookingProject = givenSomeTemporaryBookingProject(1000111); + + // when + final var result = jpaAttempt.transacted(() -> { + context("person-FirbySusan@example.com"); + assertThat(bookingProjectRepo.findByUuid(givenBookingProject.getUuid())).isPresent(); + + bookingProjectRepo.deleteByUuid(givenBookingProject.getUuid()); + }); + + // then + result.assertExceptionWithRootCauseMessage( + JpaSystemException.class, + "[403] Subject ", " is not allowed to delete hs_booking_project"); + assertThat(jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net"); + return bookingProjectRepo.findByUuid(givenBookingProject.getUuid()); + }).assertSuccessful().returnedValue()).isPresent(); // still there + } + + @Test + public void deletingABookingProjectAlsoDeletesRelatedRolesAndGrants() { + // given + context("superuser-alex@hostsharing.net"); + final var initialRoleNames = Array.from(distinctRoleNamesOf(rawRoleRepo.findAll())); + final var initialGrantNames = Array.from(distinctGrantDisplaysOf(rawGrantRepo.findAll())); + final var givenBookingProject = givenSomeTemporaryBookingProject(1000111); + + // when + final var result = jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net"); + return bookingProjectRepo.deleteByUuid(givenBookingProject.getUuid()); + }); + + // then + result.assertSuccessful(); + assertThat(result.returnedValue()).isEqualTo(1); + assertThat(distinctRoleNamesOf(rawRoleRepo.findAll())).containsExactlyInAnyOrder(initialRoleNames); + assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(initialGrantNames); + } + } + + @Test + public void auditJournalLogIsAvailable() { + // given + final var query = em.createNativeQuery(""" + select currentTask, targetTable, targetOp + from tx_journal_v + where targettable = 'hs_booking_project'; + """); + + // when + @SuppressWarnings("unchecked") final List customerLogEntries = query.getResultList(); + + // then + assertThat(customerLogEntries).map(Arrays::toString).contains( + "[creating booking-project test-data 1000111, hs_booking_project, INSERT]", + "[creating booking-project test-data 1000212, hs_booking_project, INSERT]", + "[creating booking-project test-data 1000313, hs_booking_project, INSERT]"); + } + + private HsBookingProjectEntity givenSomeTemporaryBookingProject(final int debitorNumber) { + return jpaAttempt.transacted(() -> { + context("superuser-alex@hostsharing.net"); + final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(debitorNumber).get(0); + final var newBookingProject = HsBookingProjectEntity.builder() + .debitor(givenDebitor) + .caption("some temp project") + .build(); + + return toCleanup(bookingProjectRepo.save(newBookingProject)); + }).assertSuccessful().returnedValue(); + } + + void exactlyTheseBookingProjectsAreReturned( + final List actualResult, + final String... bookingProjectNames) { + assertThat(actualResult) + .extracting(bookingProjectEntity -> bookingProjectEntity.toString()) + .containsExactlyInAnyOrder(bookingProjectNames); + } + + void allTheseBookingProjectsAreReturned( + final List actualResult, + final String... bookingProjectNames) { + assertThat(actualResult) + .extracting(bookingProjectEntity -> bookingProjectEntity.toString()) + .contains(bookingProjectNames); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/TestHsBookingProject.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/TestHsBookingProject.java new file mode 100644 index 00000000..e00c6aaf --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/TestHsBookingProject.java @@ -0,0 +1,15 @@ +package net.hostsharing.hsadminng.hs.booking.project; + +import lombok.experimental.UtilityClass; + +import static net.hostsharing.hsadminng.hs.office.debitor.TestHsOfficeDebitor.TEST_DEBITOR; + +@UtilityClass +public class TestHsBookingProject { + + + public static final HsBookingProjectEntity TEST_PROJECT = HsBookingProjectEntity.builder() + .debitor(TEST_DEBITOR) + .caption("test project") + .build(); +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java index f3eb66ee..d11e7278 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java @@ -5,6 +5,8 @@ import io.restassured.http.ContentType; import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; @@ -19,6 +21,7 @@ import java.util.Map; import java.util.UUID; import static java.util.Map.entry; +import static java.util.Optional.ofNullable; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; @@ -41,6 +44,9 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup @Autowired HsBookingItemRepository bookingItemRepo; + @Autowired + HsBookingProjectRepository projectRepo; + @Autowired HsOfficeDebitorRepository debitorRepo; @@ -55,14 +61,16 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup // given context("superuser-alex@hostsharing.net"); - final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(1000111).get(0); + final var givenProject = projectRepo.findAll().stream() + .filter(p -> p.getCaption().equals("D-1000111 default project")) + .findAny().orElseThrow(); RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") .port(port) .when() - .get("http://localhost/api/hs/hosting/assets?debitorUuid=" + givenDebitor.getUuid()) + .get("http://localhost/api/hs/hosting/assets?projectUuid=" + givenProject.getUuid() + "&type=MANAGED_WEBSPACE") .then().log().all().assertThat() .statusCode(200) .contentType("application/json") @@ -70,7 +78,18 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup [ { "type": "MANAGED_WEBSPACE", - "identifier": "aaa01", + "identifier": "sec01", + "caption": "some Webspace", + "config": { + "HDD": 2048, + "RAM": 1, + "SDD": 512, + "extra": 42 + } + }, + { + "type": "MANAGED_WEBSPACE", + "identifier": "fir01", "caption": "some Webspace", "config": { "HDD": 2048, @@ -80,24 +99,15 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } }, { - "type": "MANAGED_SERVER", - "identifier": "vm1011", - "caption": "some ManagedServer", + "type": "MANAGED_WEBSPACE", + "identifier": "thi01", + "caption": "some Webspace", "config": { - "CPU": 2, + "HDD": 2048, + "RAM": 1, "SDD": 512, "extra": 42 } - }, - { - "type": "CLOUD_SERVER", - "identifier": "vm2011", - "caption": "another CloudServer", - "config": { - "CPU": 2, - "HDD": 1024, - "extra": 42 - } } ] """)); @@ -158,13 +168,13 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Nested - class AddServer { + class AddAsset { @Test void globalAdmin_canAddBookedAsset() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingItem = givenBookingItem("First", "some PrivateCloud"); + final var givenBookingItem = givenBookingItem("D-1000111 default project", "some PrivateCloud"); final var location = RestAssured // @formatter:off .given() @@ -206,24 +216,27 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup void parentAssetAgent_canAddSubAsset() { context.define("superuser-alex@hostsharing.net"); - final var givenParentAsset = givenParentAsset("First", MANAGED_SERVER); + final var givenParentAsset = givenParentAsset("D-1000111 default project", MANAGED_SERVER); + + context.define("person-FirbySusan@example.com"); final var location = RestAssured // @formatter:off .given() - .header("current-user", "person-FirbySusan@example.com") - .contentType(ContentType.JSON) - .body(""" - { - "parentAssetUuid": "%s", - "type": "MANAGED_WEBSPACE", - "identifier": "fir90", - "caption": "some new ManagedWebspace in client's ManagedServer", - "config": { "SSD": 100, "Traffic": 250 } - } - """.formatted(givenParentAsset.getUuid())) - .port(port) + .header("current-user", "superuser-alex@hostsharing.net") + .header("assumed-roles", "hs_hosting_asset#vm1011:ADMIN") + .contentType(ContentType.JSON) + .body(""" + { + "parentAssetUuid": "%s", + "type": "MANAGED_WEBSPACE", + "identifier": "fir90", + "caption": "some new ManagedWebspace in client's ManagedServer", + "config": { "SSD": 100, "Traffic": 250 } + } + """.formatted(givenParentAsset.getUuid())) + .port(port) .when() - .post("http://localhost/api/hs/hosting/assets") + .post("http://localhost/api/hs/hosting/assets") .then().log().all().assertThat() .statusCode(201) .contentType(ContentType.JSON) @@ -248,7 +261,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup void additionalValidationsArePerformend_whenAddingAsset() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingItem = givenBookingItem("First", "some PrivateCloud"); + final var givenBookingItem = givenBookingItem("D-1000111 default project", "some PrivateCloud"); final var location = RestAssured // @formatter:off .given() @@ -285,7 +298,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup void globalAdmin_canGetArbitraryAsset() { context.define("superuser-alex@hostsharing.net"); final var givenAssetUuid = assetRepo.findAll().stream() - .filter(bi -> bi.getBookingItem().getDebitor().getDebitorNumber() == 1000111) + .filter(bi -> bi.getBookingItem().getProject().getDebitor().getDebitorNumber() == 1000111) .filter(item -> item.getCaption().equals("some ManagedServer")) .findAny().orElseThrow().getUuid(); @@ -314,7 +327,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup void normalUser_canNotGetUnrelatedAsset() { context.define("superuser-alex@hostsharing.net"); final var givenAssetUuid = assetRepo.findAll().stream() - .filter(bi -> bi.getBookingItem().getDebitor().getDebitorNumber() == 1000212) + .filter(bi -> bi.getBookingItem().getProject().getDebitor().getDebitorNumber() == 1000212) .map(HsHostingAssetEntity::getUuid) .findAny().orElseThrow(); @@ -332,7 +345,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup void debitorAgentUser_canGetRelatedAsset() { context.define("superuser-alex@hostsharing.net"); final var givenAssetUuid = assetRepo.findAll().stream() - .filter(bi -> bi.getBookingItem().getDebitor().getDebitorNumber() == 1000313) + .filter(bi -> bi.getBookingItem().getProject().getDebitor().getDebitorNumber() == 1000313) .filter(bi -> bi.getCaption().equals("some ManagedServer")) .findAny().orElseThrow().getUuid(); @@ -404,7 +417,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup context.define("superuser-alex@hostsharing.net"); assertThat(assetRepo.findByUuid(givenAsset.getUuid())).isPresent().get() .matches(asset -> { - assertThat(asset.toString()).isEqualTo("HsHostingAssetEntity(CLOUD_SERVER, vm2001, some test-asset, D-1000111:some CloudServer, { CPUs: 2, RAM: 100, SSD: 250, Traffic: 2000 })"); + assertThat(asset.toString()).isEqualTo("HsHostingAssetEntity(CLOUD_SERVER, vm2001, some test-asset, D-1000111:D-1000111 default project:test CloudServer, { CPUs: 2, RAM: 100, SSD: 250, Traffic: 2000 })"); return true; }); } @@ -444,7 +457,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .port(port) .when() .delete("http://localhost/api/hs/hosting/assets/" + givenAsset.getUuid()) - .then().log().body().assertThat() + .then().log().all().assertThat() .statusCode(404); // @formatter:on // then the given asset is still there @@ -452,16 +465,24 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } } - HsBookingItemEntity givenBookingItem(final String debitorName, final String bookingItemCaption) { - final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike(debitorName).stream().findAny().orElseThrow(); - return bookingItemRepo.findAllByDebitorUuid(givenDebitor.getUuid()).stream() - .filter(i -> i.getCaption().equals(bookingItemCaption)) + HsBookingItemEntity givenBookingItem(final String projectCaption, final String bookingItemCaption) { + return bookingItemRepo.findAll().stream() + .filter(a -> ofNullable(a) + .filter(bi -> bi.getCaption().equals(bookingItemCaption)) + .isPresent()) .findAny().orElseThrow(); } - HsHostingAssetEntity givenParentAsset(final String debitorName, final HsHostingAssetType assetType) { - final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike(debitorName).stream().findAny().orElseThrow(); - final var givenAsset = assetRepo.findAllByCriteria(givenDebitor.getUuid(), null, assetType).stream().findAny().orElseThrow(); + HsHostingAssetEntity givenParentAsset(final String projectCaption, final HsHostingAssetType assetType) { + final var givenAsset = assetRepo.findAll().stream() + .filter(a -> a.getType() == assetType) + .filter(a -> ofNullable(a) + .map(HsHostingAssetEntity::getBookingItem) + .map(HsBookingItemEntity::getProject) + .map(HsBookingProjectEntity::getCaption) + .filter(c -> c.equals(projectCaption)) + .isPresent()) + .findAny().orElseThrow(); return givenAsset; } @@ -473,7 +494,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup context.define("superuser-alex@hostsharing.net"); final var newAsset = HsHostingAssetEntity.builder() .uuid(UUID.randomUUID()) - .bookingItem(givenBookingItem("First", "some CloudServer")) + .bookingItem(givenBookingItem("D-1000111 default project", "test CloudServer")) .type(hostingAssetType) .identifier("vm" + identifierSuffix) .caption("some test-asset") diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityUnitTest.java index 2f0fc00a..d87d14f0 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityUnitTest.java @@ -37,7 +37,7 @@ class HsHostingAssetEntityUnitTest { final var result = givenServer.toString(); assertThat(result).isEqualTo( - "HsHostingAssetEntity(MANAGED_WEBSPACE, xyz00, some managed webspace, MANAGED_SERVER:vm1234, D-1000100:test booking item, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })"); + "HsHostingAssetEntity(MANAGED_WEBSPACE, xyz00, some managed webspace, MANAGED_SERVER:vm1234, D-1000100:test project:test booking item, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })"); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java index 83a07599..e5408b4f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; @@ -44,6 +45,9 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu @Autowired HsBookingItemRepository bookingItemRepo; + @Autowired + HsBookingProjectRepository projectRepo; + @Autowired HsOfficeDebitorRepository debitorRepo; @@ -70,7 +74,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu // given context("superuser-alex@hostsharing.net"); final var count = assetRepo.count(); - final var givenManagedServer = givenManagedServer("First", MANAGED_SERVER); + final var givenManagedServer = givenManagedServer("D-1000111 default project", MANAGED_SERVER); // when final var result = attempt(em, () -> { @@ -99,7 +103,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()).stream() .map(s -> s.replace("hs_office_", "")) .toList(); - final var givenBookingItem = givenBookingItem("First", "some PrivateCloud"); + final var givenBookingItem = givenBookingItem("D-1000111 default project", "some PrivateCloud"); // when final var result = attempt(em, () -> { @@ -117,27 +121,30 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu final var all = rawRoleRepo.findAll(); assertThat(distinctRoleNamesOf(all)).containsExactlyInAnyOrder(Array.from( initialRoleNames, - "hs_hosting_asset#D-1000111-somePrivateCloud-vm9000:ADMIN", - "hs_hosting_asset#D-1000111-somePrivateCloud-vm9000:OWNER", - "hs_hosting_asset#D-1000111-somePrivateCloud-vm9000:TENANT")); + "hs_hosting_asset#vm9000:OWNER", + "hs_hosting_asset#vm9000:ADMIN", + "hs_hosting_asset#vm9000:AGENT", + "hs_hosting_asset#vm9000:TENANT")); assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())) .map(s -> s.replace("hs_office_", "")) .containsExactlyInAnyOrder(fromFormatted( initialGrantNames, // owner - "{ grant perm:hs_hosting_asset#D-1000111-somePrivateCloud-vm9000:DELETE to role:hs_hosting_asset#D-1000111-somePrivateCloud-vm9000:OWNER by system and assume }", - "{ grant role:hs_hosting_asset#D-1000111-somePrivateCloud-vm9000:OWNER to role:hs_booking_item#D-1000111-somePrivateCloud:ADMIN by system and assume }", + "{ grant role:hs_hosting_asset#vm9000:OWNER to role:hs_booking_item#somePrivateCloud:ADMIN by system and assume }", + "{ grant perm:hs_hosting_asset#vm9000:DELETE to role:hs_hosting_asset#vm9000:OWNER by system and assume }", + "{ grant role:hs_hosting_asset#vm9000:ADMIN to role:hs_hosting_asset#vm9000:OWNER by system and assume }", // admin - "{ grant perm:hs_hosting_asset#D-1000111-somePrivateCloud-vm9000:INSERT>hs_hosting_asset to role:hs_hosting_asset#D-1000111-somePrivateCloud-vm9000:ADMIN by system and assume }", - "{ grant perm:hs_hosting_asset#D-1000111-somePrivateCloud-vm9000:UPDATE to role:hs_hosting_asset#D-1000111-somePrivateCloud-vm9000:ADMIN by system and assume }", - "{ grant role:hs_hosting_asset#D-1000111-somePrivateCloud-vm9000:ADMIN to role:hs_hosting_asset#D-1000111-somePrivateCloud-vm9000:OWNER by system and assume }", + "{ grant perm:hs_hosting_asset#vm9000:INSERT>hs_hosting_asset to role:hs_hosting_asset#vm9000:ADMIN by system and assume }", + "{ grant perm:hs_hosting_asset#vm9000:UPDATE to role:hs_hosting_asset#vm9000:ADMIN by system and assume }", + "{ grant role:hs_hosting_asset#vm9000:ADMIN to role:hs_booking_item#somePrivateCloud:AGENT by system and assume }", + "{ grant role:hs_hosting_asset#vm9000:TENANT to role:hs_hosting_asset#vm9000:AGENT by system and assume }", + "{ grant role:hs_hosting_asset#vm9000:AGENT to role:hs_hosting_asset#vm9000:ADMIN by system and assume }", // tenant - "{ grant perm:hs_hosting_asset#D-1000111-somePrivateCloud-vm9000:SELECT to role:hs_hosting_asset#D-1000111-somePrivateCloud-vm9000:TENANT by system and assume }", - "{ grant role:hs_booking_item#D-1000111-somePrivateCloud:TENANT to role:hs_hosting_asset#D-1000111-somePrivateCloud-vm9000:TENANT by system and assume }", - "{ grant role:hs_hosting_asset#D-1000111-somePrivateCloud-vm9000:TENANT to role:hs_hosting_asset#D-1000111-somePrivateCloud-vm9000:ADMIN by system and assume }", + "{ grant perm:hs_hosting_asset#vm9000:SELECT to role:hs_hosting_asset#vm9000:TENANT by system and assume }", + "{ grant role:hs_booking_item#somePrivateCloud:TENANT to role:hs_hosting_asset#vm9000:TENANT by system and assume }", null)); } @@ -162,26 +169,28 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu // then allTheseServersAreReturned( result, - "HsHostingAssetEntity(MANAGED_WEBSPACE, bbb01, some Webspace, MANAGED_SERVER:vm1012, D-1000212:some ManagedServer, { HDD: 2048, RAM: 1, SDD: 512, extra: 42 })", - "HsHostingAssetEntity(MANAGED_WEBSPACE, aaa01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:some ManagedServer, { HDD: 2048, RAM: 1, SDD: 512, extra: 42 })", - "HsHostingAssetEntity(MANAGED_WEBSPACE, ccc01, some Webspace, MANAGED_SERVER:vm1013, D-1000313:some ManagedServer, { HDD: 2048, RAM: 1, SDD: 512, extra: 42 })"); + "HsHostingAssetEntity(MANAGED_WEBSPACE, sec01, some Webspace, MANAGED_SERVER:vm1012, D-1000212:D-1000212 default project:separate ManagedServer, { HDD: 2048, RAM: 1, SDD: 512, extra: 42 })", + "HsHostingAssetEntity(MANAGED_WEBSPACE, thi01, some Webspace, MANAGED_SERVER:vm1013, D-1000313:D-1000313 default project:separate ManagedServer, { HDD: 2048, RAM: 1, SDD: 512, extra: 42 })", + "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedServer, { HDD: 2048, RAM: 1, SDD: 512, extra: 42 })"); } @Test public void normalUser_canViewOnlyRelatedAsset() { // given: context("person-FirbySusan@example.com"); - final var debitorUuid = debitorRepo.findDebitorByDebitorNumber(1000111).stream().findAny().orElseThrow().getUuid(); + final var projectUuid = projectRepo.findAll().stream() + .filter(p -> p.getCaption().equals("D-1000111 default project")) + .findAny().orElseThrow().getUuid(); // when: - final var result = assetRepo.findAllByCriteria(debitorUuid, null, null); + final var result = assetRepo.findAllByCriteria(projectUuid, null, null); // then: exactlyTheseAssetsAreReturned( result, - "HsHostingAssetEntity(MANAGED_WEBSPACE, aaa01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:some ManagedServer, { HDD: 2048, RAM: 1, SDD: 512, extra: 42 })", - "HsHostingAssetEntity(MANAGED_SERVER, vm1011, some ManagedServer, D-1000111:some PrivateCloud, { CPU: 2, SDD: 512, extra: 42 })", - "HsHostingAssetEntity(CLOUD_SERVER, vm2011, another CloudServer, D-1000111:some PrivateCloud, { CPU: 2, HDD: 1024, extra: 42 })"); + "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedServer, { HDD: 2048, RAM: 1, SDD: 512, extra: 42 })", + "HsHostingAssetEntity(MANAGED_SERVER, vm1011, some ManagedServer, D-1000111:D-1000111 default project:some PrivateCloud, { CPU: 2, SDD: 512, extra: 42 })", + "HsHostingAssetEntity(CLOUD_SERVER, vm2011, another CloudServer, D-1000111:D-1000111 default project:some PrivateCloud, { CPU: 2, HDD: 1024, extra: 42 })"); } @Test @@ -197,7 +206,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu // then allTheseServersAreReturned( result, - "HsHostingAssetEntity(MANAGED_WEBSPACE, aaa01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:some ManagedServer, { HDD: 2048, RAM: 1, SDD: 512, extra: 42 })"); + "HsHostingAssetEntity(MANAGED_WEBSPACE, thi01, some Webspace, MANAGED_SERVER:vm1013, D-1000313:D-1000313 default project:separate ManagedServer, { HDD: 2048, RAM: 1, SDD: 512, extra: 42 })"); } } @@ -208,7 +217,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu @Test public void hostsharingAdmin_canUpdateArbitraryServer() { // given - final var givenAssetUuid = givenSomeTemporaryAsset("First", "vm1000").getUuid(); + final var givenAssetUuid = givenSomeTemporaryAsset("D-1000111 default project", "vm1000").getUuid(); // when final var result = jpaAttempt.transacted(() -> { @@ -242,7 +251,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu public void globalAdmin_withoutAssumedRole_canDeleteAnyAsset() { // given context("superuser-alex@hostsharing.net", null); - final var givenAsset = givenSomeTemporaryAsset("First", "vm1000"); + final var givenAsset = givenSomeTemporaryAsset("D-1000111 default project", "vm1000"); // when final var result = jpaAttempt.transacted(() -> { @@ -262,7 +271,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu public void relatedOwner_canDeleteTheirRelatedAsset() { // given context("superuser-alex@hostsharing.net", null); - final var givenAsset = givenSomeTemporaryAsset("First", "vm1000"); + final var givenAsset = givenSomeTemporaryAsset("D-1000111 default project", "vm1000"); // when final var result = jpaAttempt.transacted(() -> { @@ -284,11 +293,11 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu public void relatedAdmin_canNotDeleteTheirRelatedAsset() { // given context("superuser-alex@hostsharing.net", null); - final var givenAsset = givenSomeTemporaryAsset("First", "vm1000"); + final var givenAsset = givenSomeTemporaryAsset("D-1000111 default project", "vm1000"); // when final var result = jpaAttempt.transacted(() -> { - context("person-FirbySusan@example.com", "hs_hosting_asset#D-1000111-someCloudServer-vm1000:ADMIN"); + context("person-FirbySusan@example.com", "hs_hosting_asset#vm1000:ADMIN"); assertThat(assetRepo.findByUuid(givenAsset.getUuid())).isPresent(); assetRepo.deleteByUuid(givenAsset.getUuid()); @@ -310,7 +319,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu context("superuser-alex@hostsharing.net"); final var initialRoleNames = Array.from(distinctRoleNamesOf(rawRoleRepo.findAll())); final var initialGrantNames = Array.from(distinctGrantDisplaysOf(rawGrantRepo.findAll())); - final var givenAsset = givenSomeTemporaryAsset("First", "vm1000"); + final var givenAsset = givenSomeTemporaryAsset("D-1000111 default project", "vm1000"); // when final var result = jpaAttempt.transacted(() -> { @@ -340,15 +349,15 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu // then assertThat(customerLogEntries).map(Arrays::toString).contains( - "[creating hosting-asset test-data 1000111, hs_hosting_asset, INSERT]", - "[creating hosting-asset test-data 1000212, hs_hosting_asset, INSERT]", - "[creating hosting-asset test-data 1000313, hs_hosting_asset, INSERT]"); + "[creating hosting-asset test-data D-1000111 default project, hs_hosting_asset, INSERT]", + "[creating hosting-asset test-data D-1000212 default project, hs_hosting_asset, INSERT]", + "[creating hosting-asset test-data D-1000313 default project, hs_hosting_asset, INSERT]"); } - private HsHostingAssetEntity givenSomeTemporaryAsset(final String debitorName, final String identifier) { + private HsHostingAssetEntity givenSomeTemporaryAsset(final String projectCaption, final String identifier) { return jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net"); - final var givenBookingItem = givenBookingItem(debitorName, "some CloudServer"); + final var givenBookingItem = givenBookingItem("D-1000111 default project", "some PrivateCloud"); final var newAsset = HsHostingAssetEntity.builder() .bookingItem(givenBookingItem) .type(CLOUD_SERVER) @@ -363,16 +372,20 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu }).assertSuccessful().returnedValue(); } - HsBookingItemEntity givenBookingItem(final String debitorName, final String bookingItemCaption) { - final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike(debitorName).stream().findAny().orElseThrow(); - return bookingItemRepo.findAllByDebitorUuid(givenDebitor.getUuid()).stream() + HsBookingItemEntity givenBookingItem(final String projectCaption, final String bookingItemCaption) { + final var givenProject = projectRepo.findAll().stream() + .filter(p -> p.getCaption().equals(projectCaption)) + .findAny().orElseThrow(); + return bookingItemRepo.findAllByProjectUuid(givenProject.getUuid()).stream() .filter(i -> i.getCaption().equals(bookingItemCaption)) .findAny().orElseThrow(); } - HsHostingAssetEntity givenManagedServer(final String debitorName, final HsHostingAssetType type) { - final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike(debitorName).stream().findAny().orElseThrow(); - return assetRepo.findAllByCriteria(givenDebitor.getUuid(), null, type).stream() + HsHostingAssetEntity givenManagedServer(final String projectCaption, final HsHostingAssetType type) { + final var givenProject = projectRepo.findAll().stream() + .filter(p -> p.getCaption().equals(projectCaption)) + .findAny().orElseThrow(); + return assetRepo.findAllByCriteria(givenProject.getUuid(), null, type).stream() .findAny().orElseThrow(); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidatorUnitTest.java index e0397036..53088072 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidatorUnitTest.java @@ -2,7 +2,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; -import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import org.junit.jupiter.api.Test; import java.util.Map; @@ -12,12 +11,12 @@ import static java.util.Map.entry; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; import static org.assertj.core.api.Assertions.assertThat; +import static net.hostsharing.hsadminng.hs.booking.project.TestHsBookingProject.TEST_PROJECT; class HsManagedWebspaceHostingAssetValidatorUnitTest { final HsBookingItemEntity managedServerBookingItem = HsBookingItemEntity.builder() - .debitor(HsOfficeDebitorEntity.builder().defaultPrefix("abc").build() - ) + .project(TEST_PROJECT) .build(); final HsHostingAssetEntity mangedServerAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_SERVER) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java index c234a680..b2e54d06 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java @@ -181,7 +181,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean .containsExactlyInAnyOrder(Array.fromFormatted( initialGrantNames, "{ grant perm:relation#FirstGmbH-with-DEBITOR-FourtheG:INSERT>sepamandate to role:relation#FirstGmbH-with-DEBITOR-FourtheG:ADMIN by system and assume }", - "{ grant perm:relation#FirstGmbH-with-DEBITOR-FourtheG:INSERT>hs_booking_item to role:relation#FirstGmbH-with-DEBITOR-FourtheG:ADMIN by system and assume }", + "{ grant perm:relation#FirstGmbH-with-DEBITOR-FourtheG:INSERT>hs_booking_project to role:relation#FirstGmbH-with-DEBITOR-FourtheG:ADMIN by system and assume }", // owner "{ grant perm:debitor#D-1000122:DELETE to role:relation#FirstGmbH-with-DEBITOR-FourtheG:OWNER by system and assume }", diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/TestHsOfficeDebitor.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/TestHsOfficeDebitor.java index 4305b87a..b8ddf8b5 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/TestHsOfficeDebitor.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/TestHsOfficeDebitor.java @@ -20,5 +20,6 @@ public class TestHsOfficeDebitor { .contact(TEST_CONTACT) .build()) .partner(TEST_PARTNER) + .defaultPrefix("abc") .build(); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java index 5d2b85c6..b41e4d11 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java @@ -623,6 +623,7 @@ public class ImportOfficeData extends ContextBasedTest { context(rbacSuperuser); em.createNativeQuery("delete from hs_hosting_asset where true").executeUpdate(); em.createNativeQuery("delete from hs_booking_item where true").executeUpdate(); + em.createNativeQuery("delete from hs_booking_project where true").executeUpdate(); em.createNativeQuery("delete from hs_office_coopassetstransaction where true").executeUpdate(); em.createNativeQuery("delete from hs_office_coopassetstransaction_legacy_id where true").executeUpdate(); em.createNativeQuery("delete from hs_office_coopsharestransaction where true").executeUpdate();