From c23baca47a59a6deced960e4e708ee9c9e550749 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 3 Jun 2024 14:45:28 +0200 Subject: [PATCH 01/18] 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(); From fc2b437a55255f7a0c3b278b376809d11672d538 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 6 Jun 2024 13:46:14 +0200 Subject: [PATCH 02/18] add assigned-asset, add more hosting-asset test-data and introduce HsBookingDebitor+hs_booking_debitor_rv (#58) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/58 Reviewed-by: Marc Sandlus --- .../debitor/HsBookingDebitorEntity.java | 55 ++++++++ .../debitor/HsBookingDebitorRepository.java | 14 +++ .../hs/booking/item/HsBookingItemEntity.java | 6 +- .../booking/item/HsBookingItemRepository.java | 3 +- .../project/HsBookingProjectController.java | 16 ++- .../project/HsBookingProjectEntity.java | 7 +- .../project/HsBookingProjectRepository.java | 2 +- .../hosting/asset/HsHostingAssetEntity.java | 20 ++- .../asset/HsHostingAssetRepository.java | 3 +- .../hs/hosting/asset/HsHostingAssetType.java | 6 +- .../HsCloudServerHostingAssetValidator.java | 20 --- .../HsHostingAssetEntityValidators.java | 2 +- .../HsManagedServerHostingAssetValidator.java | 12 +- ...sManagedWebspaceHostingAssetValidator.java | 6 - .../hs-hosting/hs-hosting-asset-schemas.yaml | 4 +- .../changelog/1-rbac/1058-rbac-generators.sql | 11 +- .../6100-hs-booking-debitor.sql | 17 +++ .../6200-hs-booking-project.sql} | 0 .../6203-hs-booking-project-rbac.md} | 0 .../6203-hs-booking-project-rbac.sql} | 0 .../6208-hs-booking-project-test-data.sql} | 0 .../6200-hs-booking-item.sql | 0 .../6203-hs-booking-item-rbac.md | 0 .../6203-hs-booking-item-rbac.sql | 0 .../6208-hs-booking-item-test-data.sql | 0 .../7010-hs-hosting-asset.sql | 11 +- .../7013-hs-hosting-asset-rbac.md | 24 ++-- .../7013-hs-hosting-asset-rbac.sql | 15 ++- .../7018-hs-hosting-asset-test-data.sql | 18 ++- .../db/changelog/db.changelog-master.yaml | 14 ++- .../debitor/HsBookingDebitorEntityTest.java | 33 +++++ .../booking/debitor/TestHsBookingDebitor.java | 13 ++ ...HsBookingItemControllerAcceptanceTest.java | 39 +++--- .../item/HsBookingItemEntityUnitTest.java | 4 +- ...sBookingItemRepositoryIntegrationTest.java | 5 +- ...ookingProjectControllerAcceptanceTest.java | 27 ++-- ...HsBookingProjectEntityPatcherUnitTest.java | 4 +- .../HsBookingProjectEntityUnitTest.java | 8 +- ...okingProjectRepositoryIntegrationTest.java | 18 +-- .../booking/project/TestHsBookingProject.java | 4 +- ...sHostingAssetControllerAcceptanceTest.java | 118 +++++++----------- .../asset/HsHostingAssetEntityUnitTest.java | 40 +++++- ...ingAssetPropsControllerAcceptanceTest.java | 69 +++++----- ...HostingAssetRepositoryIntegrationTest.java | 28 ++--- ...udServerHostingAssetValidatorUnitTest.java | 19 +-- ...sHostingAssetEntityValidatorsUnitTest.java | 15 ++- ...edServerHostingAssetValidatorUnitTest.java | 16 ++- ...WebspaceHostingAssetValidatorUnitTest.java | 38 +----- ...OfficeDebitorControllerAcceptanceTest.java | 2 +- 49 files changed, 438 insertions(+), 348 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntity.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorRepository.java delete mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidator.java create mode 100644 src/main/resources/db/changelog/6-hs-booking/610-booking-debitor/6100-hs-booking-debitor.sql rename src/main/resources/db/changelog/6-hs-booking/{610-booking-project/6100-hs-booking-project.sql => 620-booking-project/6200-hs-booking-project.sql} (100%) rename src/main/resources/db/changelog/6-hs-booking/{610-booking-project/6103-hs-booking-project-rbac.md => 620-booking-project/6203-hs-booking-project-rbac.md} (100%) rename src/main/resources/db/changelog/6-hs-booking/{610-booking-project/6103-hs-booking-project-rbac.sql => 620-booking-project/6203-hs-booking-project-rbac.sql} (100%) rename src/main/resources/db/changelog/6-hs-booking/{610-booking-project/6108-hs-booking-project-test-data.sql => 620-booking-project/6208-hs-booking-project-test-data.sql} (100%) rename src/main/resources/db/changelog/6-hs-booking/{620-booking-item => 630-booking-item}/6200-hs-booking-item.sql (100%) rename src/main/resources/db/changelog/6-hs-booking/{620-booking-item => 630-booking-item}/6203-hs-booking-item-rbac.md (100%) rename src/main/resources/db/changelog/6-hs-booking/{620-booking-item => 630-booking-item}/6203-hs-booking-item-rbac.sql (100%) rename src/main/resources/db/changelog/6-hs-booking/{620-booking-item => 630-booking-item}/6208-hs-booking-item-test-data.sql (100%) create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/TestHsBookingDebitor.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntity.java new file mode 100644 index 00000000..3bc83ee6 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntity.java @@ -0,0 +1,55 @@ +package net.hostsharing.hsadminng.hs.booking.debitor; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import net.hostsharing.hsadminng.errors.DisplayName; +import net.hostsharing.hsadminng.stringify.Stringify; +import net.hostsharing.hsadminng.stringify.Stringifyable; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.UUID; + +import static net.hostsharing.hsadminng.stringify.Stringify.stringify; + +// a partial HsOfficeDebitorEntity to reduce the number of SQL queries to load the entity +@Entity +@Table(name = "hs_booking_debitor_rv") +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@DisplayName("BookingDebitor") +public class HsBookingDebitorEntity implements Stringifyable { + + public static final String DEBITOR_NUMBER_TAG = "D-"; + + private static Stringify stringify = + stringify(HsBookingDebitorEntity.class, "booking-debitor") + .withIdProp(HsBookingDebitorEntity::toShortString) + .withProp(HsBookingDebitorEntity::getDefaultPrefix) + .quotedValues(false); + + @Id + private UUID uuid; + + @Column(name = "debitornumber") + private Integer debitorNumber; + + @Column(name = "defaultprefix", columnDefinition = "char(3) not null") + private String defaultPrefix; + + @Override + public String toString() { + return stringify.apply(this); + } + + @Override + public String toShortString() { + return DEBITOR_NUMBER_TAG + debitorNumber; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorRepository.java new file mode 100644 index 00000000..f69dd72f --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorRepository.java @@ -0,0 +1,14 @@ +package net.hostsharing.hsadminng.hs.booking.debitor; + +import org.springframework.data.repository.Repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface HsBookingDebitorRepository extends Repository { + + Optional findByUuid(UUID id); + + List findByDebitorNumber(int debitorNumber); +} 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 4739c638..1c5040e7 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 @@ -160,6 +160,10 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab return resources; } + public HsBookingProjectEntity getRelatedProject() { + return project != null ? project : parentItem.getRelatedProject(); + } + public static RbacView rbac() { return rbacViewFor("bookingItem", HsBookingItemEntity.class) .withIdentityView(SQL.projection("caption")) @@ -198,6 +202,6 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab } public static void main(String[] args) throws IOException { - rbac().generateWithBaseFileName("6-hs-booking/620-booking-item/6203-hs-booking-item-rbac"); + rbac().generateWithBaseFileName("6-hs-booking/630-booking-item/6303-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 cda96233..9ee9badc 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 @@ -8,9 +8,10 @@ import java.util.UUID; public interface HsBookingItemRepository extends Repository { - List findAll(); Optional findByUuid(final UUID bookingItemUuid); + List findByCaption(String bookingItemCaption); + 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 index 10230d0b..1b614dee 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectController.java @@ -1,6 +1,7 @@ package net.hostsharing.hsadminng.hs.booking.project; import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorRepository; 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; @@ -12,8 +13,10 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import jakarta.persistence.EntityNotFoundException; import java.util.List; import java.util.UUID; +import java.util.function.BiConsumer; @RestController public class HsBookingProjectController implements HsBookingProjectsApi { @@ -27,6 +30,9 @@ public class HsBookingProjectController implements HsBookingProjectsApi { @Autowired private HsBookingProjectRepository bookingProjectRepo; + @Autowired + private HsBookingDebitorRepository debitorRepo; + @Override @Transactional(readOnly = true) public ResponseEntity> listBookingProjectsByDebitorUuid( @@ -50,7 +56,7 @@ public class HsBookingProjectController implements HsBookingProjectsApi { context.define(currentUser, assumedRoles); - final var entityToSave = mapper.map(body, HsBookingProjectEntity.class); + final var entityToSave = mapper.map(body, HsBookingProjectEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); final var saved = bookingProjectRepo.save(entityToSave); @@ -111,4 +117,12 @@ public class HsBookingProjectController implements HsBookingProjectsApi { final var mapped = mapper.map(saved, HsBookingProjectResource.class); return ResponseEntity.ok(mapped); } + + final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { + if (resource.getDebitorUuid() != null) { + entity.setDebitor(debitorRepo.findByUuid(resource.getDebitorUuid()) + .orElseThrow(() -> new EntityNotFoundException("ERROR: [400] debitorUuid %s not found".formatted( + resource.getDebitorUuid())))); + } + }; } 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 index aee3242f..b1cf4a41 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntity.java @@ -1,6 +1,7 @@ package net.hostsharing.hsadminng.hs.booking.project; import lombok.*; +import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; @@ -49,7 +50,7 @@ public class HsBookingProjectEntity implements Stringifyable, RbacObject { @ManyToOne(optional = false) @JoinColumn(name = "debitoruuid") - private HsOfficeDebitorEntity debitor; + private HsBookingDebitorEntity debitor; @Column(name = "caption") private String caption; @@ -61,7 +62,7 @@ public class HsBookingProjectEntity implements Stringifyable, RbacObject { @Override public String toShortString() { - return ofNullable(debitor).map(HsOfficeDebitorEntity::toShortString).orElse("D-???????") + + return ofNullable(debitor).map(HsBookingDebitorEntity::toShortString).orElse("D-???????") + ":" + caption; } @@ -108,6 +109,6 @@ public class HsBookingProjectEntity implements Stringifyable, RbacObject { } public static void main(String[] args) throws IOException { - rbac().generateWithBaseFileName("6-hs-booking/610-booking-project/6103-hs-booking-project-rbac"); + rbac().generateWithBaseFileName("6-hs-booking/620-booking-project/6203-hs-booking-project-rbac"); } } 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 index b224dad6..f8a171b4 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepository.java @@ -8,8 +8,8 @@ import java.util.UUID; public interface HsBookingProjectRepository extends Repository { - List findAll(); Optional findByUuid(final UUID bookingProjectUuid); + List findByCaption(final String projectCaption); List findAllByDebitorUuid(final UUID bookingProjectUuid); 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 04a812a2..8d573c48 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,9 +33,7 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; -import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; 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; @@ -65,6 +63,7 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validata .withProp(HsHostingAssetEntity::getIdentifier) .withProp(HsHostingAssetEntity::getCaption) .withProp(HsHostingAssetEntity::getParentAsset) + .withProp(HsHostingAssetEntity::getAssignedToAsset) .withProp(HsHostingAssetEntity::getBookingItem) .withProp(HsHostingAssetEntity::getConfig) .quotedValues(false); @@ -84,6 +83,10 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validata @JoinColumn(name = "parentassetuuid") private HsHostingAssetEntity parentAsset; + @ManyToOne + @JoinColumn(name = "assignedtoassetuuid") + private HsHostingAssetEntity assignedToAsset; + @Column(name = "type") @Enumerated(EnumType.STRING) private HsHostingAssetType type; @@ -144,12 +147,17 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validata NULLABLE) .toRole("bookingItem", AGENT).grantPermission(INSERT) - .importEntityAlias("parentAsset", HsHostingAssetEntity.class, usingCase(MANAGED_SERVER), + .importEntityAlias("parentAsset", HsHostingAssetEntity.class, usingDefaultCase(), dependsOnColumn("parentAssetUuid"), directlyFetchedByDependsOnColumn(), NULLABLE) .toRole("parentAsset", ADMIN).grantPermission(INSERT) + .importEntityAlias("assignedToAsset", HsHostingAssetEntity.class, usingDefaultCase(), + dependsOnColumn("assignedToAssetUuid"), + directlyFetchedByDependsOnColumn(), + NULLABLE) + .createRole(OWNER, (with) -> { with.incomingSuperRole("bookingItem", ADMIN); with.incomingSuperRole("parentAsset", ADMIN); @@ -160,13 +168,15 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validata with.incomingSuperRole("parentAsset", AGENT); with.permission(UPDATE); }) - .createSubRole(AGENT) + .createSubRole(AGENT, (with) -> { + with.outgoingSubRole("assignedToAsset", TENANT); + }) .createSubRole(TENANT, (with) -> { with.outgoingSubRole("bookingItem", TENANT); with.outgoingSubRole("parentAsset", TENANT); with.permission(SELECT); }) - .limitDiagramTo("asset", "bookingItem", "bookingItem.debitorRel", "parentServer", "global"); + .limitDiagramTo("asset", "bookingItem", "bookingItem.debitorRel", "parentAsset", "assignedToAsset", "global"); } public static void main(String[] args) throws IOException { 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 7de7726b..cefe79f6 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 @@ -10,9 +10,10 @@ import java.util.UUID; public interface HsHostingAssetRepository extends Repository { - List findAll(); Optional findByUuid(final UUID serverUuid); + List findByIdentifier(String assetIdentifier); + @Query(""" SELECT asset FROM HsHostingAssetEntity asset WHERE (:projectUuid IS NULL OR asset.bookingItem.project.uuid = :projectUuid) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java index f4040046..f02a50f0 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java @@ -6,11 +6,13 @@ public enum HsHostingAssetType { MANAGED_SERVER, // named e.g. vm1234 MANAGED_WEBSPACE(MANAGED_SERVER), // named eg. xyz00 UNIX_USER(MANAGED_WEBSPACE), // named e.g. xyz00-abc - DOMAIN_SETUP(UNIX_USER), // named e.g. example.org + DOMAIN_DNS_SETUP(MANAGED_WEBSPACE), // named e.g. example.org + DOMAIN_HTTP_SETUP(MANAGED_WEBSPACE), // named e.g. example.org + DOMAIN_EMAIL_SETUP(MANAGED_WEBSPACE), // named e.g. example.org // TODO.spec: SECURE_MX EMAIL_ALIAS(MANAGED_WEBSPACE), // named e.g. xyz00-abc - EMAIL_ADDRESS(DOMAIN_SETUP), // named e.g. sample@example.org + EMAIL_ADDRESS(DOMAIN_EMAIL_SETUP), // named e.g. sample@example.org PGSQL_USER(MANAGED_WEBSPACE), // named e.g. xyz00_abc PGSQL_DATABASE(MANAGED_WEBSPACE), // named e.g. xyz00_abc, TODO.spec: or PGSQL_USER? MARIADB_USER(MANAGED_WEBSPACE), // named e.g. xyz00_abc diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidator.java deleted file mode 100644 index 8c43dd43..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidator.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.hostsharing.hsadminng.hs.hosting.asset.validators; - -import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; -import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; -import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; - -import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty; - -class HsCloudServerHostingAssetValidator extends HsEntityValidator { - - public HsCloudServerHostingAssetValidator() { - super( - integerProperty("CPUs").min(1).max(32).required(), - integerProperty("RAM").unit("GB").min(1).max(128).required(), - integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(), - integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(), - integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required() - ); - } -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidators.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidators.java index c4eaef0f..11df9a84 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidators.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidators.java @@ -20,7 +20,7 @@ public class HsHostingAssetEntityValidators { private static final Map, HsEntityValidator> validators = new HashMap<>(); static { - register(CLOUD_SERVER, new HsCloudServerHostingAssetValidator()); + register(CLOUD_SERVER, new HsEntityValidator<>()); register(MANAGED_SERVER, new HsManagedServerHostingAssetValidator()); register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator()); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java index aee10839..35f3b81d 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java @@ -10,11 +10,13 @@ class HsManagedServerHostingAssetValidator extends HsEntityValidator { public HsManagedWebspaceHostingAssetValidator() { - super( - integerProperty("SSD").unit("GB").min(1).max(100).step(1).required(), - integerProperty("HDD").unit("GB").min(0).max(250).step(10).optional(), - integerProperty("Traffic").unit("GB").min(10).max(1000).step(10).required() - ); } @Override diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml index 7390c3c8..8e9dbe02 100644 --- a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml @@ -10,7 +10,9 @@ components: - MANAGED_SERVER - MANAGED_WEBSPACE - UNIX_USER - - DOMAIN_SETUP + - DOMAIN_DNS_SETUP + - DOMAIN_HTTP_SETUP + - DOMAIN_EMAIL_SETUP - EMAIL_ALIAS - EMAIL_ADDRESS - PGSQL_USER diff --git a/src/main/resources/db/changelog/1-rbac/1058-rbac-generators.sql b/src/main/resources/db/changelog/1-rbac/1058-rbac-generators.sql index 958d3afe..016b8f89 100644 --- a/src/main/resources/db/changelog/1-rbac/1058-rbac-generators.sql +++ b/src/main/resources/db/changelog/1-rbac/1058-rbac-generators.sql @@ -118,10 +118,13 @@ begin sql = format($sql$ create or replace function %1$sUuidByIdName(givenIdName varchar) returns uuid - language sql - strict as $f$ - select uuid from %1$s_iv iv where iv.idName = givenIdName; - $f$; + language plpgsql as $f$ + declare + singleMatch uuid; + begin + select uuid into strict singleMatch from %1$s_iv iv where iv.idName = givenIdName; + return singleMatch; + end; $f$; $sql$, targetTable); execute sql; diff --git a/src/main/resources/db/changelog/6-hs-booking/610-booking-debitor/6100-hs-booking-debitor.sql b/src/main/resources/db/changelog/6-hs-booking/610-booking-debitor/6100-hs-booking-debitor.sql new file mode 100644 index 00000000..c9dc8287 --- /dev/null +++ b/src/main/resources/db/changelog/6-hs-booking/610-booking-debitor/6100-hs-booking-debitor.sql @@ -0,0 +1,17 @@ +--liquibase formatted sql + +-- ============================================================================ +--changeset hs-booking-debitor-RESTRICTED-VIEW:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +create view hs_booking_debitor_rv as + select debitor.uuid, + debitor.version, + (partner.partnerNumber::varchar || debitor.debitorNumberSuffix)::numeric as debitorNumber, + debitor.defaultPrefix + from hs_office_debitor_rv debitor + -- RBAC for debitor is sufficient, for faster access we are bypassing RBAC for the join tables + join hs_office_relation debitorRel on debitor.debitorReluUid=debitorRel.uuid + join hs_office_relation partnerRel on partnerRel.holderUuid=debitorRel.anchorUuid + join hs_office_partner partner on partner.partnerReluUid=partnerRel.uuid; +--// 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/620-booking-project/6200-hs-booking-project.sql similarity index 100% rename from src/main/resources/db/changelog/6-hs-booking/610-booking-project/6100-hs-booking-project.sql rename to src/main/resources/db/changelog/6-hs-booking/620-booking-project/6200-hs-booking-project.sql 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/620-booking-project/6203-hs-booking-project-rbac.md similarity index 100% rename from src/main/resources/db/changelog/6-hs-booking/610-booking-project/6103-hs-booking-project-rbac.md rename to src/main/resources/db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.md diff --git a/src/main/resources/db/changelog/6-hs-booking/610-booking-project/6103-hs-booking-project-rbac.sql b/src/main/resources/db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.sql similarity index 100% rename from src/main/resources/db/changelog/6-hs-booking/610-booking-project/6103-hs-booking-project-rbac.sql rename to src/main/resources/db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.sql diff --git a/src/main/resources/db/changelog/6-hs-booking/610-booking-project/6108-hs-booking-project-test-data.sql b/src/main/resources/db/changelog/6-hs-booking/620-booking-project/6208-hs-booking-project-test-data.sql similarity index 100% rename from src/main/resources/db/changelog/6-hs-booking/610-booking-project/6108-hs-booking-project-test-data.sql rename to src/main/resources/db/changelog/6-hs-booking/620-booking-project/6208-hs-booking-project-test-data.sql diff --git a/src/main/resources/db/changelog/6-hs-booking/620-booking-item/6200-hs-booking-item.sql b/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6200-hs-booking-item.sql similarity index 100% rename from src/main/resources/db/changelog/6-hs-booking/620-booking-item/6200-hs-booking-item.sql rename to src/main/resources/db/changelog/6-hs-booking/630-booking-item/6200-hs-booking-item.sql diff --git a/src/main/resources/db/changelog/6-hs-booking/620-booking-item/6203-hs-booking-item-rbac.md b/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6203-hs-booking-item-rbac.md similarity index 100% rename from src/main/resources/db/changelog/6-hs-booking/620-booking-item/6203-hs-booking-item-rbac.md rename to src/main/resources/db/changelog/6-hs-booking/630-booking-item/6203-hs-booking-item-rbac.md 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/630-booking-item/6203-hs-booking-item-rbac.sql similarity index 100% rename from src/main/resources/db/changelog/6-hs-booking/620-booking-item/6203-hs-booking-item-rbac.sql rename to src/main/resources/db/changelog/6-hs-booking/630-booking-item/6203-hs-booking-item-rbac.sql 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/630-booking-item/6208-hs-booking-item-test-data.sql similarity index 100% rename from src/main/resources/db/changelog/6-hs-booking/620-booking-item/6208-hs-booking-item-test-data.sql rename to src/main/resources/db/changelog/6-hs-booking/630-booking-item/6208-hs-booking-item-test-data.sql 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 755dbbec..c6fedb72 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 @@ -9,7 +9,9 @@ create type HsHostingAssetType as enum ( 'MANAGED_SERVER', 'MANAGED_WEBSPACE', 'UNIX_USER', - 'DOMAIN_SETUP', + 'DOMAIN_DNS_SETUP', + 'DOMAIN_HTTP_SETUP', + 'DOMAIN_EMAIL_SETUP', 'EMAIL_ALIAS', 'EMAIL_ADDRESS', 'PGSQL_USER', @@ -27,6 +29,7 @@ create table if not exists hs_hosting_asset bookingItemUuid uuid null references hs_booking_item(uuid), type HsHostingAssetType not null, parentAssetUuid uuid null references hs_hosting_asset(uuid) initially deferred, + assignedToAssetUuid uuid null references hs_hosting_asset(uuid) initially deferred, identifier varchar(80) not null, caption varchar(80), config jsonb not null, @@ -59,9 +62,11 @@ begin when 'MANAGED_SERVER' then null when 'MANAGED_WEBSPACE' then 'MANAGED_SERVER' when 'UNIX_USER' then 'MANAGED_WEBSPACE' - when 'DOMAIN_SETUP' then 'UNIX_USER' + when 'DOMAIN_DNS_SETUP' then 'MANAGED_WEBSPACE' + when 'DOMAIN_HTTP_SETUP' then 'MANAGED_WEBSPACE' + when 'DOMAIN_EMAIL_SETUP' then 'MANAGED_WEBSPACE' when 'EMAIL_ALIAS' then 'MANAGED_WEBSPACE' - when 'EMAIL_ADDRESS' then 'DOMAIN_SETUP' + when 'EMAIL_ADDRESS' then 'DOMAIN_EMAIL_SETUP' when 'PGSQL_USER' then 'MANAGED_WEBSPACE' when 'PGSQL_DATABASE' then 'MANAGED_WEBSPACE' when 'MARIADB_USER' then 'MANAGED_WEBSPACE' 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 b9a65745..bf7780e1 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 @@ -25,40 +25,30 @@ subgraph asset["`**asset**`"] perm:asset:INSERT{{asset:INSERT}} perm:asset:DELETE{{asset:DELETE}} perm:asset:UPDATE{{asset:UPDATE}} - perm:asset:SELECT{{asset:SELECT}} end end -subgraph bookingItem["`**bookingItem**`"] +subgraph assignedToAsset["`**assignedToAsset**`"] direction TB - style bookingItem fill:#99bcdb,stroke:#274d6e,stroke-width:8px + style assignedToAsset fill:#99bcdb,stroke:#274d6e,stroke-width:8px - subgraph bookingItem:roles[ ] - style bookingItem:roles fill:#99bcdb,stroke:white + subgraph assignedToAsset:roles[ ] + style assignedToAsset:roles fill:#99bcdb,stroke:white - role:bookingItem:OWNER[[bookingItem:OWNER]] - role:bookingItem:ADMIN[[bookingItem:ADMIN]] - role:bookingItem:AGENT[[bookingItem:AGENT]] - role:bookingItem:TENANT[[bookingItem:TENANT]] + role:assignedToAsset:TENANT[[assignedToAsset:TENANT]] end end %% granting roles to roles -role:bookingItem:OWNER -.-> role:bookingItem:ADMIN -role:bookingItem:ADMIN -.-> role:bookingItem:AGENT -role:bookingItem:AGENT -.-> role:bookingItem:TENANT -role:bookingItem:ADMIN ==> role:asset:OWNER role:asset:OWNER ==> role:asset:ADMIN -role:bookingItem:AGENT ==> role:asset:ADMIN role:asset:ADMIN ==> role:asset:AGENT +role:asset:AGENT ==> role:assignedToAsset:TENANT role:asset:AGENT ==> role:asset:TENANT -role:asset:TENANT ==> role:bookingItem:TENANT +role:assignedToAsset:TENANT ==> role:asset: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 ae6c51c7..f14430a7 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 @@ -31,6 +31,7 @@ create or replace procedure buildRbacSystemForHsHostingAsset( declare newBookingItem hs_booking_item; + newAssignedToAsset hs_hosting_asset; newParentAsset hs_hosting_asset; begin @@ -38,6 +39,8 @@ begin SELECT * FROM hs_booking_item WHERE uuid = NEW.bookingItemUuid INTO newBookingItem; + SELECT * FROM hs_hosting_asset WHERE uuid = NEW.assignedToAssetUuid INTO newAssignedToAsset; + SELECT * FROM hs_hosting_asset WHERE uuid = NEW.parentAssetUuid INTO newParentAsset; perform createRoleWithGrants( @@ -59,13 +62,15 @@ begin perform createRoleWithGrants( hsHostingAssetAGENT(NEW), - incomingSuperRoles => array[hsHostingAssetADMIN(NEW)] + incomingSuperRoles => array[hsHostingAssetADMIN(NEW)], + outgoingSubRoles => array[hsHostingAssetTENANT(newAssignedToAsset)] ); perform createRoleWithGrants( hsHostingAssetTENANT(NEW), - permissions => array['SELECT'], - incomingSuperRoles => array[hsHostingAssetAGENT(NEW)], + incomingSuperRoles => array[ + hsHostingAssetAGENT(NEW), + hsHostingAssetTENANT(newAssignedToAsset)], outgoingSubRoles => array[ hsBookingItemTENANT(newBookingItem), hsHostingAssetTENANT(newParentAsset)] @@ -197,11 +202,11 @@ create or replace function new_hs_hosting_asset_grants_insert_to_hs_hosting_asse language plpgsql strict as $$ begin - if NEW.type = 'MANAGED_SERVER' then + -- unconditional for all rows in that table call grantPermissionToRole( createPermission(NEW.uuid, 'INSERT', 'hs_hosting_asset'), hsHostingAssetADMIN(NEW)); - end if; + -- end. return NEW; end; $$; 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 737b691a..964acdec 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 @@ -16,7 +16,11 @@ declare relatedDebitor hs_office_debitor; relatedPrivateCloudBookingItem hs_booking_item; relatedManagedServerBookingItem hs_booking_item; + debitorNumberSuffix varchar; + defaultPrefix varchar; managedServerUuid uuid; + managedWebspaceUuid uuid; + webUnixUserUuid uuid; begin currentTask := 'creating hosting-asset test-data ' || givenProjectCaption; call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN'); @@ -45,12 +49,18 @@ begin assert relatedManagedServerBookingItem.uuid is not null, 'relatedManagedServerBookingItem for "' || givenProjectCaption|| '" must not be null'; select uuid_generate_v4() into managedServerUuid; + select uuid_generate_v4() into managedWebspaceUuid; + select uuid_generate_v4() into webUnixUserUuid; + debitorNumberSuffix := relatedDebitor.debitorNumberSuffix; + defaultPrefix := relatedDebitor.defaultPrefix; insert into hs_hosting_asset - (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); + (uuid, bookingitemuuid, type, parentAssetUuid, assignedToAssetUuid, identifier, caption, config) + values (managedServerUuid, relatedPrivateCloudBookingItem.uuid, 'MANAGED_SERVER', null, null, 'vm10' || debitorNumberSuffix, 'some ManagedServer', '{ "extra": 42 }'::jsonb), + (uuid_generate_v4(), relatedPrivateCloudBookingItem.uuid, 'CLOUD_SERVER', null, null, 'vm20' || debitorNumberSuffix, 'another CloudServer', '{ "extra": 42 }'::jsonb), + (managedWebspaceUuid, relatedManagedServerBookingItem.uuid, 'MANAGED_WEBSPACE', managedServerUuid, null, defaultPrefix || '01', 'some Webspace', '{ "extra": 42 }'::jsonb), + (webUnixUserUuid, null, 'UNIX_USER', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some UnixUser for Website', '{ "SSD-soft-quota": "128", "SSD-hard-quota": "256", "HDD-soft-quota": "512", "HDD-hard-quota": "1024", "extra": 42 }'::jsonb), + (uuid_generate_v4(), null, 'DOMAIN_HTTP_SETUP', managedWebspaceUuid, webUnixUserUuid, defaultPrefix || '.example.org', 'some Domain-HTTP-Setup', '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*", "extra": 42 }'::jsonb); end; $$; --// diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index aebf347d..d6b4942b 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -130,17 +130,19 @@ databaseChangeLog: - include: file: db/changelog/5-hs-office/512-coopassets/5128-hs-office-coopassets-test-data.sql - include: - file: db/changelog/6-hs-booking/610-booking-project/6100-hs-booking-project.sql + file: db/changelog/6-hs-booking/610-booking-debitor/6100-hs-booking-debitor.sql - include: - file: db/changelog/6-hs-booking/610-booking-project/6103-hs-booking-project-rbac.sql + file: db/changelog/6-hs-booking/620-booking-project/6200-hs-booking-project.sql - include: - file: db/changelog/6-hs-booking/610-booking-project/6108-hs-booking-project-test-data.sql + file: db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.sql - include: - file: db/changelog/6-hs-booking/620-booking-item/6200-hs-booking-item.sql + file: db/changelog/6-hs-booking/620-booking-project/6208-hs-booking-project-test-data.sql - include: - file: db/changelog/6-hs-booking/620-booking-item/6203-hs-booking-item-rbac.sql + file: db/changelog/6-hs-booking/630-booking-item/6200-hs-booking-item.sql - include: - file: db/changelog/6-hs-booking/620-booking-item/6208-hs-booking-item-test-data.sql + file: db/changelog/6-hs-booking/630-booking-item/6203-hs-booking-item-rbac.sql + - include: + file: db/changelog/6-hs-booking/630-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/hs/booking/debitor/HsBookingDebitorEntityTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityTest.java new file mode 100644 index 00000000..4275c56c --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityTest.java @@ -0,0 +1,33 @@ +package net.hostsharing.hsadminng.hs.booking.debitor; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class HsBookingDebitorEntityTest { + + @Test + void toStringContainsDebitorNumberAndDefaultPrefix() { + final var given = HsBookingDebitorEntity.builder() + .debitorNumber(1234567) + .defaultPrefix("som") + .build(); + + final var result = given.toString(); + + assertThat(result).isEqualTo("booking-debitor(D-1234567: som)"); + } + + @Test + void toShortStringContainsDefaultPrefix() { + final var given = HsBookingDebitorEntity.builder() + .debitorNumber(1234567) + .defaultPrefix("som") + .build(); + + final var result = given.toShortString(); + + assertThat(result).isEqualTo("D-1234567"); + } + +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/TestHsBookingDebitor.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/TestHsBookingDebitor.java new file mode 100644 index 00000000..2dcc6c3b --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/TestHsBookingDebitor.java @@ -0,0 +1,13 @@ +package net.hostsharing.hsadminng.hs.booking.debitor; + +import lombok.experimental.UtilityClass; + + +@UtilityClass +public class TestHsBookingDebitor { + + public static final HsBookingDebitorEntity TEST_BOOKING_DEBITOR = HsBookingDebitorEntity.builder() + .debitorNumber(1234500) + .defaultPrefix("abc") + .build(); +} 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 7f385824..a0054b4f 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 @@ -6,7 +6,6 @@ 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,8 +16,6 @@ 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.time.LocalDate; import java.util.List; import java.util.Map; @@ -53,9 +50,6 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup @Autowired JpaAttempt jpaAttempt; - @PersistenceContext - EntityManager em; - @Nested class ListBookingItems { @@ -180,10 +174,10 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void globalAdmin_canGetArbitraryBookingItem() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingItemUuid = bookingItemRepo.findAll().stream() - .filter(bi -> belongsToDebitorNumber(bi, 1000111)) - .filter(item -> item.getCaption().equals("some ManagedWebspace")) - .findAny().orElseThrow().getUuid(); + final var givenBookingItemUuid = bookingItemRepo.findByCaption("some ManagedWebspace").stream() + .filter(bi -> belongsToDebitorWithDefaultPrefix(bi, "fir")) + .map(HsBookingItemEntity::getUuid) + .findAny().orElseThrow(); RestAssured // @formatter:off .given() @@ -213,8 +207,8 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void normalUser_canNotGetUnrelatedBookingItem() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingItemUuid = bookingItemRepo.findAll().stream() - .filter(bi -> belongsToDebitorNumber(bi, 1000212)) + final var givenBookingItemUuid = bookingItemRepo.findByCaption("separate ManagedServer").stream() + .filter(bi -> belongsToDebitorWithDefaultPrefix(bi, "sec")) .map(HsBookingItemEntity::getUuid) .findAny().orElseThrow(); @@ -229,16 +223,18 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Test - void debitorAgentUser_canGetRelatedBookingItem() { + // TODO.impl: For unknown reason, this test fails in about 50%, not finding the uuid (404), maybe no SELECT permission? + void projectAdmin_canGetRelatedBookingItem() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingItemUuid = bookingItemRepo.findAll().stream() - .filter(bi -> belongsToDebitorNumber(bi, 1000313)) - .filter(item -> item.getCaption().equals("separate ManagedServer")) - .findAny().orElseThrow().getUuid(); + final var givenBookingItemUuid = bookingItemRepo.findByCaption("separate ManagedServer").stream() + .filter(bi -> belongsToDebitorWithDefaultPrefix(bi, "thi")) + .map(HsBookingItemEntity::getUuid) + .findAny().orElseThrow(); RestAssured // @formatter:off .given() - .header("current-user", "person-TuckerJack@example.com") + .header("current-user", "superuser-alex@hostsharing.net") + .header("assumed-roles", "hs_booking_project#D-1000313-D-1000313defaultproject:ADMIN") .port(port) .when() .get("http://localhost/api/hs/booking/items/" + givenBookingItemUuid) @@ -261,12 +257,11 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup """)); // @formatter:on } - private static boolean belongsToDebitorNumber(final HsBookingItemEntity bi, final int i) { + private static boolean belongsToDebitorWithDefaultPrefix(final HsBookingItemEntity bi, final String defaultPrefix) { return ofNullable(bi) .map(HsBookingItemEntity::getProject) .map(HsBookingProjectEntity::getDebitor) - .map(HsOfficeDebitorEntity::getDebitorNumber) - .filter(debitorNumber -> debitorNumber == i) + .map(bd -> bd.getDefaultPrefix().equals(defaultPrefix)) .isPresent(); } } @@ -317,7 +312,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup context.define("superuser-alex@hostsharing.net"); assertThat(bookingItemRepo.findByUuid(givenBookingItem.getUuid())).isPresent().get() .matches(mandate -> { - assertThat(mandate.getProject().getDebitor().toString()).isEqualTo("debitor(D-1000111: rel(anchor='LP First GmbH', type='DEBITOR', holder='LP First GmbH'), fir)"); + assertThat(mandate.getProject().getDebitor().toString()).isEqualTo("booking-debitor(D-1000111: fir)"); assertThat(mandate.getValidFrom()).isEqualTo("2022-11-01"); assertThat(mandate.getValidTo()).isEqualTo("2022-12-31"); return true; 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 f311bd09..1b95dc8a 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 @@ -29,14 +29,14 @@ class HsBookingItemEntityUnitTest { void toStringContainsAllPropertiesAndResourcesSortedByKey() { final var result = givenBookingItem.toString(); - 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 })"); + assertThat(result).isEqualTo("HsBookingItemEntity(D-1234500: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:test project:some caption"); + assertThat(result).isEqualTo("D-1234500: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 f4ac6fee..0d1e22ac 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 @@ -174,8 +174,8 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then allTheseBookingItemsAreReturned( result, - "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, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SDD: 512, Traffic: 42 })", "HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10240, SDD: 10240, Traffic: 42 })"); } @@ -326,8 +326,7 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup private HsBookingItemEntity givenSomeTemporaryBookingItem(final String projectCaption) { return jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net"); - final var givenProject = projectRepo.findAll().stream() - .filter(p -> p.getCaption().equals(projectCaption)) + final var givenProject = projectRepo.findByCaption(projectCaption).stream() .findAny().orElseThrow(); final var newBookingItem = HsBookingItemEntity.builder() .project(givenProject) 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 index 31bd8ba0..9a4c2391 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectControllerAcceptanceTest.java @@ -3,7 +3,7 @@ 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.hs.booking.debitor.HsBookingDebitorRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Nested; @@ -15,10 +15,8 @@ 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; @@ -40,7 +38,7 @@ class HsBookingProjectControllerAcceptanceTest extends ContextBasedTestWithClean HsBookingProjectRepository projectRepo; @Autowired - HsOfficeDebitorRepository debitorRepo; + HsBookingDebitorRepository debitorRepo; @Autowired JpaAttempt jpaAttempt; @@ -56,7 +54,7 @@ class HsBookingProjectControllerAcceptanceTest extends ContextBasedTestWithClean // given context("superuser-alex@hostsharing.net"); - final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(1000111).stream() + final var givenDebitor = debitorRepo.findByDebitorNumber(1000111).stream() .findFirst() .orElseThrow(); @@ -87,7 +85,7 @@ class HsBookingProjectControllerAcceptanceTest extends ContextBasedTestWithClean void globalAdmin_canAddBookingProject() { context.define("superuser-alex@hostsharing.net"); - final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(1000111).stream() + final var givenDebitor = debitorRepo.findByDebitorNumber(1000111).stream() .findFirst() .orElseThrow(); @@ -128,8 +126,7 @@ class HsBookingProjectControllerAcceptanceTest extends ContextBasedTestWithClean @Test void globalAdmin_canGetArbitraryBookingProject() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingProjectUuid = bookingProjectRepo.findAll().stream() - .filter(project -> project.getDebitor().getDebitorNumber() == 1000111) + final var givenBookingProjectUuid = bookingProjectRepo.findByCaption("D-1000111 default project").stream() .findAny().orElseThrow().getUuid(); RestAssured // @formatter:off @@ -151,8 +148,7 @@ class HsBookingProjectControllerAcceptanceTest extends ContextBasedTestWithClean @Test void normalUser_canNotGetUnrelatedBookingProject() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingProjectUuid = bookingProjectRepo.findAll().stream() - .filter(project -> project.getDebitor().getDebitorNumber() == 1000212) + final var givenBookingProjectUuid = bookingProjectRepo.findByCaption("D-1000212 default project").stream() .map(HsBookingProjectEntity::getUuid) .findAny().orElseThrow(); @@ -169,8 +165,7 @@ class HsBookingProjectControllerAcceptanceTest extends ContextBasedTestWithClean @Test void debitorAgentUser_canGetRelatedBookingProject() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingProjectUuid = bookingProjectRepo.findAll().stream() - .filter(project -> project.getDebitor().getDebitorNumber() == 1000313) + final var givenBookingProjectUuid = bookingProjectRepo.findByCaption("D-1000313 default project").stream() .findAny().orElseThrow().getUuid(); RestAssured // @formatter:off @@ -223,7 +218,7 @@ class HsBookingProjectControllerAcceptanceTest extends ContextBasedTestWithClean 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)"); + assertThat(mandate.getDebitor().toString()).isEqualTo("booking-debitor(D-1000111: fir)"); return true; }); } @@ -272,7 +267,7 @@ class HsBookingProjectControllerAcceptanceTest extends ContextBasedTestWithClean 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 givenDebitor = debitorRepo.findByDebitorNumber(debitorNumber).stream().findAny().orElseThrow(); final var newBookingProject = HsBookingProjectEntity.builder() .uuid(UUID.randomUUID()) .debitor(givenDebitor) @@ -282,8 +277,4 @@ class HsBookingProjectControllerAcceptanceTest extends ContextBasedTestWithClean 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 index cb059fe2..37229d26 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntityPatcherUnitTest.java @@ -13,7 +13,7 @@ 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 net.hostsharing.hsadminng.hs.booking.debitor.TestHsBookingDebitor.TEST_BOOKING_DEBITOR; import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -46,7 +46,7 @@ class HsBookingProjectEntityPatcherUnitTest extends PatchUnitTestBase< protected HsBookingProjectEntity newInitialEntity() { final var entity = new HsBookingProjectEntity(); entity.setUuid(INITIAL_BOOKING_PROJECT_UUID); - entity.setDebitor(TEST_DEBITOR); + entity.setDebitor(TEST_BOOKING_DEBITOR); entity.setCaption(INITIAL_CAPTION); return entity; } 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 index dd911a8a..1d53070b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntityUnitTest.java @@ -2,12 +2,12 @@ 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 net.hostsharing.hsadminng.hs.booking.debitor.TestHsBookingDebitor.TEST_BOOKING_DEBITOR; import static org.assertj.core.api.Assertions.assertThat; class HsBookingProjectEntityUnitTest { final HsBookingProjectEntity givenBookingProject = HsBookingProjectEntity.builder() - .debitor(TEST_DEBITOR) + .debitor(TEST_BOOKING_DEBITOR) .caption("some caption") .build(); @@ -15,13 +15,13 @@ class HsBookingProjectEntityUnitTest { void toStringContainsAllPropertiesAndResourcesSortedByKey() { final var result = givenBookingProject.toString(); - assertThat(result).isEqualTo("HsBookingProjectEntity(D-1000100, some caption)"); + assertThat(result).isEqualTo("HsBookingProjectEntity(D-1234500, some caption)"); } @Test void toShortStringContainsOnlyMemberNumberAndCaption() { final var result = givenBookingProject.toShortString(); - assertThat(result).isEqualTo("D-1000100:some caption"); + assertThat(result).isEqualTo("D-1234500: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 index edc4649a..70676f84 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java @@ -1,7 +1,7 @@ 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.hs.booking.debitor.HsBookingDebitorRepository; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; import net.hostsharing.hsadminng.rbac.test.Array; @@ -38,7 +38,7 @@ class HsBookingProjectRepositoryIntegrationTest extends ContextBasedTestWithClea HsBookingProjectRepository projectRepo; @Autowired - HsOfficeDebitorRepository debitorRepo; + HsBookingDebitorRepository debitorRepo; @Autowired RawRbacRoleRepository rawRoleRepo; @@ -63,7 +63,7 @@ class HsBookingProjectRepositoryIntegrationTest extends ContextBasedTestWithClea // given context("superuser-alex@hostsharing.net"); final var count = bookingProjectRepo.count(); - final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("First").get(0); + final var givenDebitor = debitorRepo.findByDebitorNumber(1000111).get(0); // when final var result = attempt(em, () -> { @@ -92,7 +92,7 @@ class HsBookingProjectRepositoryIntegrationTest extends ContextBasedTestWithClea // when attempt(em, () -> { - final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("First").get(0); + final var givenDebitor = debitorRepo.findByDebitorNumber(1000111).get(0); final var newBookingProject = HsBookingProjectEntity.builder() .debitor(givenDebitor) .caption("some new booking project") @@ -148,7 +148,7 @@ class HsBookingProjectRepositoryIntegrationTest extends ContextBasedTestWithClea public void globalAdmin_withoutAssumedRole_canViewAllBookingProjectsOfArbitraryDebitor() { // given context("superuser-alex@hostsharing.net"); - final var debitorUuid = debitorRepo.findDebitorByDebitorNumber(1000212).stream() + final var debitorUuid = debitorRepo.findByDebitorNumber(1000212).stream() .findAny().orElseThrow().getUuid(); // when @@ -164,7 +164,7 @@ class HsBookingProjectRepositoryIntegrationTest extends ContextBasedTestWithClea public void normalUser_canViewOnlyRelatedBookingProjects() { // given: context("person-FirbySusan@example.com"); - final var debitorUuid = debitorRepo.findDebitorByDebitorNumber(1000111).stream() + final var debitorUuid = debitorRepo.findByDebitorNumber(1000111).stream() .findAny().orElseThrow().getUuid(); // when: @@ -298,7 +298,7 @@ class HsBookingProjectRepositoryIntegrationTest extends ContextBasedTestWithClea private HsBookingProjectEntity givenSomeTemporaryBookingProject(final int debitorNumber) { return jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net"); - final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(debitorNumber).get(0); + final var givenDebitor = debitorRepo.findByDebitorNumber(debitorNumber).get(0); final var newBookingProject = HsBookingProjectEntity.builder() .debitor(givenDebitor) .caption("some temp project") @@ -312,7 +312,7 @@ class HsBookingProjectRepositoryIntegrationTest extends ContextBasedTestWithClea final List actualResult, final String... bookingProjectNames) { assertThat(actualResult) - .extracting(bookingProjectEntity -> bookingProjectEntity.toString()) + .extracting(HsBookingProjectEntity::toString) .containsExactlyInAnyOrder(bookingProjectNames); } @@ -320,7 +320,7 @@ class HsBookingProjectRepositoryIntegrationTest extends ContextBasedTestWithClea final List actualResult, final String... bookingProjectNames) { assertThat(actualResult) - .extracting(bookingProjectEntity -> bookingProjectEntity.toString()) + .extracting(HsBookingProjectEntity::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 index e00c6aaf..6190c36b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/TestHsBookingProject.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/TestHsBookingProject.java @@ -2,14 +2,14 @@ package net.hostsharing.hsadminng.hs.booking.project; import lombok.experimental.UtilityClass; -import static net.hostsharing.hsadminng.hs.office.debitor.TestHsOfficeDebitor.TEST_DEBITOR; +import static net.hostsharing.hsadminng.hs.booking.debitor.TestHsBookingDebitor.TEST_BOOKING_DEBITOR; @UtilityClass public class TestHsBookingProject { public static final HsBookingProjectEntity TEST_PROJECT = HsBookingProjectEntity.builder() - .debitor(TEST_DEBITOR) + .debitor(TEST_BOOKING_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 d11e7278..5204a1ec 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,7 +5,6 @@ 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; @@ -21,7 +20,6 @@ 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; @@ -61,8 +59,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup // given context("superuser-alex@hostsharing.net"); - final var givenProject = projectRepo.findAll().stream() - .filter(p -> p.getCaption().equals("D-1000111 default project")) + final var givenProject = projectRepo.findByCaption("D-1000111 default project").stream() .findAny().orElseThrow(); RestAssured // @formatter:off @@ -81,9 +78,6 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "identifier": "sec01", "caption": "some Webspace", "config": { - "HDD": 2048, - "RAM": 1, - "SDD": 512, "extra": 42 } }, @@ -92,9 +86,6 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "identifier": "fir01", "caption": "some Webspace", "config": { - "HDD": 2048, - "RAM": 1, - "SDD": 512, "extra": 42 } }, @@ -103,9 +94,6 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "identifier": "thi01", "caption": "some Webspace", "config": { - "HDD": 2048, - "RAM": 1, - "SDD": 512, "extra": 42 } } @@ -136,18 +124,6 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "identifier": "vm1011", "caption": "some ManagedServer", "config": { - "CPU": 2, - "SDD": 512, - "extra": 42 - } - }, - { - "type": "MANAGED_SERVER", - "identifier": "vm1013", - "caption": "some ManagedServer", - "config": { - "CPU": 2, - "SDD": 512, "extra": 42 } }, @@ -156,8 +132,14 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "identifier": "vm1012", "caption": "some ManagedServer", "config": { - "CPU": 2, - "SDD": 512, + "extra": 42 + } + }, + { + "type": "MANAGED_SERVER", + "identifier": "vm1013", + "caption": "some ManagedServer", + "config": { "extra": 42 } } @@ -186,7 +168,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "type": "MANAGED_SERVER", "identifier": "vm1400", "caption": "some new ManagedServer", - "config": { "CPUs": 2, "RAM": 100, "SSD": 300, "Traffic": 250 } + "config": { "monit_max_ssd_usage": 80, "monit_max_cpu_usage": 90, "monit_max_ram_usage": 70 } } """.formatted(givenBookingItem.getUuid())) .port(port) @@ -200,7 +182,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "type": "MANAGED_SERVER", "identifier": "vm1400", "caption": "some new ManagedServer", - "config": { "CPUs": 2, "RAM": 100, "SSD": 300, "Traffic": 250 } + "config": { "monit_max_ssd_usage": 80, "monit_max_cpu_usage": 90, "monit_max_ram_usage": 70 } } """)) .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*")) @@ -216,7 +198,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup void parentAssetAgent_canAddSubAsset() { context.define("superuser-alex@hostsharing.net"); - final var givenParentAsset = givenParentAsset("D-1000111 default project", MANAGED_SERVER); + final var givenParentAsset = givenParentAsset(MANAGED_SERVER, "vm1011"); context.define("person-FirbySusan@example.com"); @@ -231,7 +213,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "type": "MANAGED_WEBSPACE", "identifier": "fir90", "caption": "some new ManagedWebspace in client's ManagedServer", - "config": { "SSD": 100, "Traffic": 250 } + "config": {} } """.formatted(givenParentAsset.getUuid())) .port(port) @@ -245,7 +227,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "type": "MANAGED_WEBSPACE", "identifier": "fir90", "caption": "some new ManagedWebspace in client's ManagedServer", - "config": { "SSD": 100, "Traffic": 250 } + "config": {} } """)) .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*")) @@ -263,7 +245,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup context.define("superuser-alex@hostsharing.net"); final var givenBookingItem = givenBookingItem("D-1000111 default project", "some PrivateCloud"); - final var location = RestAssured // @formatter:off + RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") .contentType(ContentType.JSON) @@ -273,7 +255,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "type": "MANAGED_SERVER", "identifier": "vm1400", "caption": "some new ManagedServer", - "config": { "CPUs": 0, "extra": 42 } + "config": { "monit_max_ssd_usage": 0, "monit_max_cpu_usage": 101, "extra": 42 } } """.formatted(givenBookingItem.getUuid())) .port(port) @@ -285,7 +267,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .body("", lenientlyEquals(""" { "statusPhrase": "Bad Request", - "message": "['config.extra' is not expected but is set to '42', 'config.CPUs' is expected to be >= 1 but is 0, 'config.RAM' is required but missing, 'config.SSD' is required but missing, 'config.Traffic' is required but missing]" + "message": "['config.extra' is not expected but is set to '42', 'config.monit_max_ssd_usage' is expected to be >= 10 but is 0, 'config.monit_max_cpu_usage' is expected to be <= 100 but is 101, 'config.monit_max_ram_usage' is required but missing]" } """)); // @formatter:on } @@ -297,9 +279,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void globalAdmin_canGetArbitraryAsset() { context.define("superuser-alex@hostsharing.net"); - final var givenAssetUuid = assetRepo.findAll().stream() - .filter(bi -> bi.getBookingItem().getProject().getDebitor().getDebitorNumber() == 1000111) - .filter(item -> item.getCaption().equals("some ManagedServer")) + final var givenAssetUuid = assetRepo.findByIdentifier("vm1011").stream() + .filter(bi -> bi.getBookingItem().getProject().getCaption().equals("D-1000111 default project")) .findAny().orElseThrow().getUuid(); RestAssured // @formatter:off @@ -315,8 +296,6 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup { "caption": "some ManagedServer", "config": { - "CPU": 2, - "SDD": 512, "extra": 42 } } @@ -326,8 +305,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void normalUser_canNotGetUnrelatedAsset() { context.define("superuser-alex@hostsharing.net"); - final var givenAssetUuid = assetRepo.findAll().stream() - .filter(bi -> bi.getBookingItem().getProject().getDebitor().getDebitorNumber() == 1000212) + final var givenAssetUuid = assetRepo.findByIdentifier("vm1012").stream() + .filter(bi -> bi.getBookingItem().getProject().getCaption().equals("D-1000212 default project")) .map(HsHostingAssetEntity::getUuid) .findAny().orElseThrow(); @@ -344,9 +323,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void debitorAgentUser_canGetRelatedAsset() { context.define("superuser-alex@hostsharing.net"); - final var givenAssetUuid = assetRepo.findAll().stream() - .filter(bi -> bi.getBookingItem().getProject().getDebitor().getDebitorNumber() == 1000313) - .filter(bi -> bi.getCaption().equals("some ManagedServer")) + final var givenAssetUuid = assetRepo.findByIdentifier("vm1013").stream() + .filter(bi -> bi.getBookingItem().getProject().getCaption().equals("D-1000313 default project")) .findAny().orElseThrow().getUuid(); RestAssured // @formatter:off @@ -363,8 +341,6 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "identifier": "vm1013", "caption": "some ManagedServer", "config": { - "CPU": 2, - "SDD": 512, "extra": 42 } } @@ -378,8 +354,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void globalAdmin_canPatchAllUpdatablePropertiesOfAsset() { - final var givenAsset = givenSomeTemporaryHostingAsset("2001", CLOUD_SERVER, - config("CPUs", 4), config("RAM", 100), config("HDD", 100), config("Traffic", 2000)); + final var givenAsset = givenSomeTemporaryHostingAsset("2001", MANAGED_SERVER, + config("monit_max_ssd_usage", 80), config("monit_max_hdd_usage", 90), config("monit_max_cpu_usage", 90), config("monit_max_ram_usage", 70)); RestAssured // @formatter:off .given() @@ -388,9 +364,9 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .body(""" { "config": { - "CPUs": 2, - "HDD": null, - "SSD": 250 + "monit_max_ssd_usage": 85, + "monit_max_hdd_usage": null, + "monit_min_free_ssd": 5 } } """) @@ -402,13 +378,14 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .contentType(ContentType.JSON) .body("", lenientlyEquals(""" { - "type": "CLOUD_SERVER", + "type": "MANAGED_SERVER", "identifier": "vm2001", "caption": "some test-asset", "config": { - "CPUs": 2, - "RAM": 100, - "SSD": 250 + "monit_max_cpu_usage": 90, + "monit_max_ram_usage": 70, + "monit_max_ssd_usage": 85, + "monit_min_free_ssd": 5 } } """)); // @formatter:on @@ -417,7 +394,8 @@ 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:D-1000111 default project:test CloudServer, { CPUs: 2, RAM: 100, SSD: 250, Traffic: 2000 })"); + assertThat(asset.toString()).isEqualTo( + "HsHostingAssetEntity(MANAGED_SERVER, vm2001, some test-asset, D-1000111:D-1000111 default project:some ManagedServer, { monit_max_cpu_usage: 90, monit_max_ram_usage: 70, monit_max_ssd_usage: 85, monit_min_free_ssd: 5 })"); return true; }); } @@ -429,8 +407,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void globalAdmin_canDeleteArbitraryAsset() { context.define("superuser-alex@hostsharing.net"); - final var givenAsset = givenSomeTemporaryHostingAsset("2002", CLOUD_SERVER, - config("CPUs", 4), config("RAM", 100), config("HDD", 100), config("Traffic", 2000)); + final var givenAsset = givenSomeTemporaryHostingAsset("1002", MANAGED_SERVER, + config("monit_max_ssd_usage", 80), config("monit_max_hdd_usage", 90), config("monit_max_cpu_usage", 90), config("monit_max_ram_usage", 70)); RestAssured // @formatter:off .given() @@ -448,8 +426,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void normalUser_canNotDeleteUnrelatedAsset() { context.define("superuser-alex@hostsharing.net"); - final var givenAsset = givenSomeTemporaryHostingAsset("2003", CLOUD_SERVER, - config("CPUs", 4), config("RAM", 100), config("HDD", 100), config("Traffic", 2000)); + final var givenAsset = givenSomeTemporaryHostingAsset("1003", MANAGED_SERVER, + config("monit_max_ssd_usage", 80), config("monit_max_hdd_usage", 90), config("monit_max_cpu_usage", 90), config("monit_max_ram_usage", 70)); RestAssured // @formatter:off .given() @@ -466,22 +444,14 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } HsBookingItemEntity givenBookingItem(final String projectCaption, final String bookingItemCaption) { - return bookingItemRepo.findAll().stream() - .filter(a -> ofNullable(a) - .filter(bi -> bi.getCaption().equals(bookingItemCaption)) - .isPresent()) + return bookingItemRepo.findByCaption(bookingItemCaption).stream() + .filter(bi -> bi.getRelatedProject().getCaption().contains(projectCaption)) .findAny().orElseThrow(); } - HsHostingAssetEntity givenParentAsset(final String projectCaption, final HsHostingAssetType assetType) { - final var givenAsset = assetRepo.findAll().stream() + HsHostingAssetEntity givenParentAsset(final HsHostingAssetType assetType, final String assetIdentifier) { + final var givenAsset = assetRepo.findByIdentifier(assetIdentifier).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; } @@ -494,7 +464,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup context.define("superuser-alex@hostsharing.net"); final var newAsset = HsHostingAssetEntity.builder() .uuid(UUID.randomUUID()) - .bookingItem(givenBookingItem("D-1000111 default project", "test CloudServer")) + .bookingItem(givenBookingItem("D-1000111 default project", "some ManagedServer")) .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 d87d14f0..e45bdb5b 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 @@ -20,7 +20,7 @@ class HsHostingAssetEntityUnitTest { entry("SSD-storage", 512), entry("HDD-storage", 2048))) .build(); - final HsHostingAssetEntity givenServer = HsHostingAssetEntity.builder() + final HsHostingAssetEntity givenWebspace = HsHostingAssetEntity.builder() .bookingItem(TEST_BOOKING_ITEM) .type(HsHostingAssetType.MANAGED_WEBSPACE) .parentAsset(givenParentAsset) @@ -31,19 +31,47 @@ class HsHostingAssetEntityUnitTest { entry("SSD-storage", 512), entry("HDD-storage", 2048))) .build(); + final HsHostingAssetEntity givenUnixUser = HsHostingAssetEntity.builder() + .type(HsHostingAssetType.UNIX_USER) + .parentAsset(givenWebspace) + .identifier("xyz00-web") + .caption("some unix-user") + .config(Map.ofEntries( + entry("SSD-soft-quota", 128), + entry("SSD-hard-quota", 256), + entry("HDD-soft-quota", 256), + entry("HDD-hard-quota", 512))) + .build(); + final HsHostingAssetEntity givenDomainHttpSetup = HsHostingAssetEntity.builder() + .type(HsHostingAssetType.DOMAIN_HTTP_SETUP) + .parentAsset(givenWebspace) + .identifier("example.org") + .assignedToAsset(givenUnixUser) + .caption("some domain setup") + .config(Map.ofEntries( + entry("option-htdocsfallback", true), + entry("use-fcgiphpbin", "/usr/lib/cgi-bin/php"), + entry("validsubdomainnames", "*"))) + .build(); @Test void toStringContainsAllPropertiesAndResourcesSortedByKey() { - final var result = givenServer.toString(); - assertThat(result).isEqualTo( - "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 })"); + assertThat(givenWebspace.toString()).isEqualTo( + "HsHostingAssetEntity(MANAGED_WEBSPACE, xyz00, some managed webspace, MANAGED_SERVER:vm1234, D-1234500:test project:test booking item, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })"); + + assertThat(givenUnixUser.toString()).isEqualTo( + "HsHostingAssetEntity(UNIX_USER, xyz00-web, some unix-user, MANAGED_WEBSPACE:xyz00, { HDD-hard-quota: 512, HDD-soft-quota: 256, SSD-hard-quota: 256, SSD-soft-quota: 128 })"); + + assertThat(givenDomainHttpSetup.toString()).isEqualTo( + "HsHostingAssetEntity(DOMAIN_HTTP_SETUP, example.org, some domain setup, MANAGED_WEBSPACE:xyz00, UNIX_USER:xyz00-web, { option-htdocsfallback: true, use-fcgiphpbin: /usr/lib/cgi-bin/php, validsubdomainnames: * })"); } @Test void toShortStringContainsOnlyMemberNumberAndCaption() { - final var result = givenServer.toShortString(); - assertThat(result).isEqualTo("MANAGED_WEBSPACE:xyz00"); + assertThat(givenWebspace.toShortString()).isEqualTo("MANAGED_WEBSPACE:xyz00"); + assertThat(givenUnixUser.toShortString()).isEqualTo("UNIX_USER:xyz00-web"); + assertThat(givenDomainHttpSetup.toShortString()).isEqualTo("DOMAIN_HTTP_SETUP:example.org"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java index e6cc9acd..55c2e29e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java @@ -54,48 +54,57 @@ class HsHostingAssetPropsControllerAcceptanceTest { [ { "type": "integer", - "propertyName": "CPUs", - "required": true, + "propertyName": "monit_min_free_ssd", + "required": false, "unit": null, "min": 1, - "max": 32, - "step": null - }, - { - "type": "integer", - "propertyName": "RAM", - "required": true, - "unit": "GB", - "min": 1, - "max": 128, - "step": null - }, - { - "type": "integer", - "propertyName": "SSD", - "required": true, - "unit": "GB", - "min": 25, "max": 1000, - "step": 25 + "step": null }, { "type": "integer", - "propertyName": "HDD", + "propertyName": "monit_min_free_hdd", "required": false, - "unit": "GB", - "min": 0, + "unit": null, + "min": 1, "max": 4000, - "step": 250 + "step": null }, { "type": "integer", - "propertyName": "Traffic", + "propertyName": "monit_max_ssd_usage", "required": true, - "unit": "GB", - "min": 250, - "max": 10000, - "step": 250 + "unit": "%", + "min": 10, + "max": 100, + "step": null + }, + { + "type": "integer", + "propertyName": "monit_max_hdd_usage", + "required": false, + "unit": "%", + "min": 10, + "max": 100, + "step": null + }, + { + "type": "integer", + "propertyName": "monit_max_cpu_usage", + "required": true, + "unit": "%", + "min": 10, + "max": 100, + "step": null + }, + { + "type": "integer", + "propertyName": "monit_max_ram_usage", + "required": true, + "unit": "%", + "min": 10, + "max": 100, + "step": null } ] """)); 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 e5408b4f..f781046a 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 @@ -4,7 +4,6 @@ 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; import net.hostsharing.hsadminng.rbac.test.Array; @@ -48,9 +47,6 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu @Autowired HsBookingProjectRepository projectRepo; - @Autowired - HsOfficeDebitorRepository debitorRepo; - @Autowired RawRbacRoleRepository rawRoleRepo; @@ -143,7 +139,6 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu "{ grant role:hs_hosting_asset#vm9000:AGENT to role:hs_hosting_asset#vm9000:ADMIN by system and assume }", // tenant - "{ 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)); @@ -169,17 +164,16 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu // then allTheseServersAreReturned( result, - "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 })"); + "HsHostingAssetEntity(MANAGED_WEBSPACE, sec01, some Webspace, MANAGED_SERVER:vm1012, D-1000212:D-1000212 default project:separate ManagedServer, { extra: 42 })", + "HsHostingAssetEntity(MANAGED_WEBSPACE, thi01, some Webspace, MANAGED_SERVER:vm1013, D-1000313:D-1000313 default project:separate ManagedServer, { extra: 42 })", + "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedServer, { extra: 42 })"); } @Test public void normalUser_canViewOnlyRelatedAsset() { // given: context("person-FirbySusan@example.com"); - final var projectUuid = projectRepo.findAll().stream() - .filter(p -> p.getCaption().equals("D-1000111 default project")) + final var projectUuid = projectRepo.findByCaption("D-1000111 default project").stream() .findAny().orElseThrow().getUuid(); // when: @@ -188,9 +182,9 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu // then: exactlyTheseAssetsAreReturned( result, - "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 })"); + "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedServer, { extra: 42 })", + "HsHostingAssetEntity(MANAGED_SERVER, vm1011, some ManagedServer, D-1000111:D-1000111 default project:some PrivateCloud, { extra: 42 })", + "HsHostingAssetEntity(CLOUD_SERVER, vm2011, another CloudServer, D-1000111:D-1000111 default project:some PrivateCloud, { extra: 42 })"); } @Test @@ -206,7 +200,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu // then allTheseServersAreReturned( result, - "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, thi01, some Webspace, MANAGED_SERVER:vm1013, D-1000313:D-1000313 default project:separate ManagedServer, { extra: 42 })"); } } @@ -373,8 +367,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu } HsBookingItemEntity givenBookingItem(final String projectCaption, final String bookingItemCaption) { - final var givenProject = projectRepo.findAll().stream() - .filter(p -> p.getCaption().equals(projectCaption)) + final var givenProject = projectRepo.findByCaption(projectCaption).stream() .findAny().orElseThrow(); return bookingItemRepo.findAllByProjectUuid(givenProject.getUuid()).stream() .filter(i -> i.getCaption().equals(bookingItemCaption)) @@ -382,8 +375,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu } HsHostingAssetEntity givenManagedServer(final String projectCaption, final HsHostingAssetType type) { - final var givenProject = projectRepo.findAll().stream() - .filter(p -> p.getCaption().equals(projectCaption)) + final var givenProject = projectRepo.findByCaption(projectCaption).stream() .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/HsCloudServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java index ee77c565..de679c40 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java @@ -18,10 +18,7 @@ class HsCloudServerHostingAssetValidatorUnitTest { final var cloudServerHostingAssetEntity = HsHostingAssetEntity.builder() .type(CLOUD_SERVER) .config(Map.ofEntries( - entry("RAM", 2000), - entry("SSD", 256), - entry("Traffic", "250"), - entry("SLA-Platform", "xxx") + entry("RAM", 2000) )) .build(); final var validator = forType(cloudServerHostingAssetEntity.getType()); @@ -31,12 +28,7 @@ class HsCloudServerHostingAssetValidatorUnitTest { final var result = validator.validate(cloudServerHostingAssetEntity); // then - assertThat(result).containsExactlyInAnyOrder( - "'config.SLA-Platform' is not expected but is set to 'xxx'", - "'config.CPUs' is required but missing", - "'config.RAM' is expected to be <= 128 but is 2000", - "'config.SSD' is expected to be multiple of 25 but is 256", - "'config.Traffic' is expected to be of type class java.lang.Integer, but is of type 'String'"); + assertThat(result).containsExactly("'config.RAM' is not expected but is set to '2000'"); } @Test @@ -45,11 +37,6 @@ class HsCloudServerHostingAssetValidatorUnitTest { final var validator = forType(CLOUD_SERVER); // then - assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( - "{type=integer, propertyName=CPUs, required=true, unit=null, min=1, max=32, step=null}", - "{type=integer, propertyName=RAM, required=true, unit=GB, min=1, max=128, step=null}", - "{type=integer, propertyName=SSD, required=true, unit=GB, min=25, max=1000, step=25}", - "{type=integer, propertyName=HDD, required=false, unit=GB, min=0, max=4000, step=250}", - "{type=integer, propertyName=Traffic, required=true, unit=GB, min=250, max=10000, step=250}"); + assertThat(validator.properties()).map(Map::toString).isEmpty(); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorsUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorsUnitTest.java index 07eb7517..0e07e30c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorsUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorsUnitTest.java @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Test; import jakarta.validation.ValidationException; -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.validators.HsHostingAssetEntityValidators.valid; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; @@ -15,19 +15,18 @@ class HsHostingAssetEntityValidatorsUnitTest { @Test void validThrowsException() { // given - final var cloudServerHostingAssetEntity = HsHostingAssetEntity.builder() - .type(CLOUD_SERVER) + final var managedServerHostingAssetEntity = HsHostingAssetEntity.builder() + .type(MANAGED_SERVER) .build(); // when - final var result = catchThrowable( ()-> valid(cloudServerHostingAssetEntity) ); + final var result = catchThrowable( ()-> valid(managedServerHostingAssetEntity) ); // then assertThat(result).isInstanceOf(ValidationException.class) .hasMessageContaining( - "'config.CPUs' is required but missing", - "'config.RAM' is required but missing", - "'config.SSD' is required but missing", - "'config.Traffic' is required but missing"); + "'config.monit_max_ssd_usage' is required but missing", + "'config.monit_max_cpu_usage' is required but missing", + "'config.monit_max_ram_usage' is required but missing"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java index a9ee1433..cb9e066b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java @@ -18,10 +18,9 @@ class HsManagedServerHostingAssetValidatorUnitTest { final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_SERVER) .config(Map.ofEntries( - entry("RAM", 2000), - entry("SSD", 256), - entry("Traffic", "250"), - entry("SLA-Platform", "xxx") + entry("monit_max_hdd_usage", "90"), + entry("monit_max_cpu_usage", 2), + entry("monit_max_ram_usage", 101) )) .build(); final var validator = forType(mangedWebspaceHostingAssetEntity.getType()); @@ -31,10 +30,9 @@ class HsManagedServerHostingAssetValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( - "'config.SLA-Platform' is not expected but is set to 'xxx'", - "'config.CPUs' is required but missing", - "'config.RAM' is expected to be <= 128 but is 2000", - "'config.SSD' is expected to be multiple of 25 but is 256", - "'config.Traffic' is expected to be of type class java.lang.Integer, but is of type 'String'"); + "'config.monit_max_ssd_usage' is required but missing", + "'config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'", + "'config.monit_max_cpu_usage' is expected to be >= 10 but is 2", + "'config.monit_max_ram_usage' is expected to be <= 100 but is 101"); } } 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 53088072..83634501 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 @@ -6,7 +6,6 @@ import org.junit.jupiter.api.Test; import java.util.Map; -import static java.util.Collections.emptyMap; 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; @@ -36,11 +35,6 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { .type(MANAGED_WEBSPACE) .parentAsset(mangedServerAssetEntity) .identifier("xyz00") - .config(Map.ofEntries( - entry("HDD", 0), - entry("SSD", 1), - entry("Traffic", 10) - )) .build(); // when @@ -50,28 +44,6 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { assertThat(result).containsExactly("'identifier' expected to match '^abc[0-9][0-9]$', but is 'xyz00'"); } - - @Test - void validatesMissingProperties() { - // given - final var validator = HsHostingAssetEntityValidators.forType(MANAGED_WEBSPACE); - final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() - .type(MANAGED_WEBSPACE) - .parentAsset(mangedServerAssetEntity) - .identifier("abc00") - .config(emptyMap()) - .build(); - - // when - final var result = validator.validate(mangedWebspaceHostingAssetEntity); - - // then - assertThat(result).containsExactlyInAnyOrder( - "'config.SSD' is required but missing", - "'config.Traffic' is required but missing" - ); - } - @Test void validatesUnknownProperties() { // given @@ -81,9 +53,6 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { .parentAsset(mangedServerAssetEntity) .identifier("abc00") .config(Map.ofEntries( - entry("HDD", 0), - entry("SSD", 1), - entry("Traffic", 10), entry("unknown", "some value") )) .build(); @@ -96,18 +65,13 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { } @Test - void validatesValidProperties() { + void validatesValidEntity() { // given final var validator = HsHostingAssetEntityValidators.forType(MANAGED_WEBSPACE); final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_WEBSPACE) .parentAsset(mangedServerAssetEntity) .identifier("abc00") - .config(Map.ofEntries( - entry("HDD", 200), - entry("SSD", 25), - entry("Traffic", 250) - )) .build(); // when diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java index 27f9f2c8..1408a87d 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java @@ -745,7 +745,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net"); final var count = em.createQuery( - "DELETE FROM HsOfficeDebitorEntity d WHERE d.debitorNumberSuffix >= " + LOWEST_TEMP_DEBITOR_SUFFIX) + "DELETE FROM HsBookingDebitorEntity d WHERE d.debitorNumberSuffix >= " + LOWEST_TEMP_DEBITOR_SUFFIX) .executeUpdate(); System.out.printf("deleted %d entities%n", count); }); From 46dc653174c4ea386dc4d71e80110c2d1e7e6ad1 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 14 Jun 2024 16:48:00 +0200 Subject: [PATCH 03/18] hierarchical-validation-baseline (#59) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/59 Reviewed-by: Marc Sandlus --- .../hsadminng/errors/CustomErrorResponse.java | 2 +- .../errors/MultiValidationException.java | 19 ++ .../RestResponseEntityExceptionHandler.java | 5 +- .../booking/item/HsBookingItemController.java | 12 +- .../hs/booking/item/HsBookingItemEntity.java | 25 +- .../HsBookingItemEntityValidator.java | 77 +++++++ ...HsBookingItemEntityValidatorRegistry.java} | 31 +-- .../HsCloudServerBookingItemValidator.java | 12 +- .../HsManagedServerBookingItemValidator.java | 22 +- ...HsManagedWebspaceBookingItemValidator.java | 92 +++++++- .../HsPrivateCloudBookingItemValidator.java | 18 ++ .../asset/HsHostingAssetController.java | 6 +- .../hosting/asset/HsHostingAssetEntity.java | 20 +- .../asset/HsHostingAssetPropsController.java | 7 +- .../HsHostingAssetEntityValidator.java | 76 +++++++ ...HsHostingAssetEntityValidatorRegistry.java | 50 ++++ .../HsHostingAssetEntityValidators.java | 51 ----- .../HsManagedServerHostingAssetValidator.java | 8 +- ...sManagedWebspaceHostingAssetValidator.java | 21 +- ...OfficeCoopAssetsTransactionController.java | 7 +- ...OfficeCoopSharesTransactionController.java | 7 +- .../hs/validation/BooleanProperty.java | 46 ++++ .../validation/BooleanPropertyValidator.java | 42 ---- .../hs/validation/EnumerationProperty.java | 44 ++++ .../EnumerationPropertyValidator.java | 38 ---- .../hs/validation/HsEntityValidator.java | 67 ++++-- .../hs/validation/HsPropertyValidator.java | 67 ------ .../hs/validation/IntegerProperty.java | 56 +++++ .../validation/IntegerPropertyValidator.java | 42 ---- .../hsadminng/hs/validation/Validatable.java | 13 -- .../hs/validation/ValidatableProperty.java | 172 ++++++++++++++ .../hostsharing/hsadminng/mapper}/Array.java | 8 +- .../rbacgrant/RbacGrantsDiagramService.java | 9 +- .../6208-hs-booking-item-test-data.sql | 14 +- .../7010-hs-hosting-asset.sql | 22 +- .../7018-hs-hosting-asset-test-data.sql | 48 ++-- .../hsadminng/arch/ArchitectureTest.java | 13 +- ...esponseEntityExceptionHandlerUnitTest.java | 2 +- ...va => HsBookingDebitorEntityUnitTest.java} | 2 +- ...HsBookingItemControllerAcceptanceTest.java | 45 ++-- .../HsBookingItemEntityPatcherUnitTest.java | 4 +- ...sBookingItemRepositoryIntegrationTest.java | 16 +- .../HsBookingItemEntityValidatorUnitTest.java | 55 +++++ ...HsBookingItemEntityValidatorsUnitTest.java | 44 ---- ...oudServerBookingItemValidatorUnitTest.java | 89 +++++++- ...gedServerBookingItemValidatorUnitTest.java | 204 +++++++++++++++-- ...dWebspaceBookingItemValidatorUnitTest.java | 40 ++-- ...vateCloudBookingItemValidatorUnitTest.java | 112 +++++++++ ...okingProjectRepositoryIntegrationTest.java | 4 +- ...sHostingAssetControllerAcceptanceTest.java | 213 ++++++++++++------ .../HsHostingAssetEntityPatcherUnitTest.java | 4 +- ...ingAssetPropsControllerAcceptanceTest.java | 29 ++- ...HostingAssetRepositoryIntegrationTest.java | 92 ++++---- ...udServerHostingAssetValidatorUnitTest.java | 8 +- ...sHostingAssetEntityValidatorUnitTest.java} | 12 +- ...edServerHostingAssetValidatorUnitTest.java | 12 +- ...WebspaceHostingAssetValidatorUnitTest.java | 30 ++- ...eBankAccountRepositoryIntegrationTest.java | 2 +- ...fficeContactRepositoryIntegrationTest.java | 2 +- ...sTransactionRepositoryIntegrationTest.java | 2 +- ...sTransactionRepositoryIntegrationTest.java | 2 +- ...fficeDebitorRepositoryIntegrationTest.java | 2 +- ...ceMembershipRepositoryIntegrationTest.java | 2 +- ...fficePartnerRepositoryIntegrationTest.java | 2 +- ...OfficePersonRepositoryIntegrationTest.java | 2 +- ...ficeRelationRepositoryIntegrationTest.java | 2 +- ...eSepaMandateRepositoryIntegrationTest.java | 4 +- .../rbac/context/ContextIntegrationTests.java | 2 +- .../RbacRoleRepositoryIntegrationTest.java | 2 +- .../RbacUserRepositoryIntegrationTest.java | 2 +- 70 files changed, 1620 insertions(+), 694 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java rename src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/{HsBookingItemEntityValidators.java => HsBookingItemEntityValidatorRegistry.java} (56%) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java delete mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidators.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java delete mode 100644 src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanPropertyValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java delete mode 100644 src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationPropertyValidator.java delete mode 100644 src/main/java/net/hostsharing/hsadminng/hs/validation/HsPropertyValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java delete mode 100644 src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerPropertyValidator.java delete mode 100644 src/main/java/net/hostsharing/hsadminng/hs/validation/Validatable.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java rename src/{test/java/net/hostsharing/hsadminng/rbac/test => main/java/net/hostsharing/hsadminng/mapper}/Array.java (83%) rename src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/{HsBookingDebitorEntityTest.java => HsBookingDebitorEntityUnitTest.java} (95%) create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorUnitTest.java delete mode 100644 src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorsUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidatorUnitTest.java rename src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/{HsHostingAssetEntityValidatorsUnitTest.java => HsHostingAssetEntityValidatorUnitTest.java} (60%) diff --git a/src/main/java/net/hostsharing/hsadminng/errors/CustomErrorResponse.java b/src/main/java/net/hostsharing/hsadminng/errors/CustomErrorResponse.java index 2714b817..9b182137 100644 --- a/src/main/java/net/hostsharing/hsadminng/errors/CustomErrorResponse.java +++ b/src/main/java/net/hostsharing/hsadminng/errors/CustomErrorResponse.java @@ -9,7 +9,7 @@ import org.springframework.web.context.request.WebRequest; import java.time.LocalDateTime; @Getter -class CustomErrorResponse { +public class CustomErrorResponse { static ResponseEntity errorResponse( final WebRequest request, diff --git a/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java b/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java new file mode 100644 index 00000000..9a6d459d --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java @@ -0,0 +1,19 @@ +package net.hostsharing.hsadminng.errors; + +import jakarta.validation.ValidationException; +import java.util.List; + +import static java.lang.String.join; + +public class MultiValidationException extends ValidationException { + + private MultiValidationException(final List violations) { + super("[\n" + join(",\n", violations) + "\n]"); + } + + public static void throwInvalid(final List violations) { + if (!violations.isEmpty()) { + throw new MultiValidationException(violations); + } + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java b/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java index 5d675484..d4d6e8bf 100644 --- a/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java +++ b/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java @@ -73,9 +73,10 @@ public class RestResponseEntityExceptionHandler } @ExceptionHandler({ Iban4jException.class, ValidationException.class }) - protected ResponseEntity handleIbanAndBicExceptions( + protected ResponseEntity handleValidationExceptions( final Throwable exc, final WebRequest request) { - final var message = line(NestedExceptionUtils.getMostSpecificCause(exc).getMessage(), 0); + final String fullMessage = NestedExceptionUtils.getMostSpecificCause(exc).getMessage(); + final var message = exc instanceof MultiValidationException ? fullMessage : line(fullMessage, 0); return errorResponse(request, HttpStatus.BAD_REQUEST, message); } 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 2ada5e0c..1343378c 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 @@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsA import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource; import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource; +import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry; import net.hostsharing.hsadminng.mapper.KeyValueMap; import net.hostsharing.hsadminng.mapper.Mapper; import org.springframework.beans.factory.annotation.Autowired; @@ -13,11 +14,12 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; -import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.valid; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; @RestController @@ -32,6 +34,9 @@ public class HsBookingItemController implements HsBookingItemsApi { @Autowired private HsBookingItemRepository bookingItemRepo; + @PersistenceContext + private EntityManager em; + @Override @Transactional(readOnly = true) public ResponseEntity> listBookingItemsByProjectUuid( @@ -57,7 +62,7 @@ public class HsBookingItemController implements HsBookingItemsApi { final var entityToSave = mapper.map(body, HsBookingItemEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); - final var saved = bookingItemRepo.save(valid(entityToSave)); + final var saved = HsBookingItemEntityValidatorRegistry.validated(bookingItemRepo.save(entityToSave)); final var uri = MvcUriComponentsBuilder.fromController(getClass()) @@ -78,6 +83,7 @@ public class HsBookingItemController implements HsBookingItemsApi { context.define(currentUser, assumedRoles); final var result = bookingItemRepo.findByUuid(bookingItemUuid); + result.ifPresent(entity -> em.detach(entity)); // prevent further LAZY-loading return result .map(bookingItemEntity -> ResponseEntity.ok( mapper.map(bookingItemEntity, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER))) @@ -112,7 +118,7 @@ public class HsBookingItemController implements HsBookingItemsApi { new HsBookingItemEntityPatcher(current).apply(body); - final var saved = bookingItemRepo.save(valid(current)); + final var saved = bookingItemRepo.save(HsBookingItemEntityValidatorRegistry.validated(current)); final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(mapped); } 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 1c5040e7..b820c243 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 @@ -10,7 +10,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity; -import net.hostsharing.hsadminng.hs.validation.Validatable; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import net.hostsharing.hsadminng.mapper.PatchableMapWrapper; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; @@ -19,6 +19,7 @@ import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; import org.hibernate.annotations.Type; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -27,12 +28,14 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import jakarta.persistence.Transient; import jakarta.persistence.Version; import java.io.IOException; import java.time.LocalDate; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -62,7 +65,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @Setter @NoArgsConstructor @AllArgsConstructor -public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatable { +public class HsBookingItemEntity implements Stringifyable, RbacObject { private static Stringify stringify = stringify(HsBookingItemEntity.class) .withProp(HsBookingItemEntity::getProject) @@ -105,6 +108,14 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab @Column(columnDefinition = "resources") private Map resources = new HashMap<>(); + @OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true) + @JoinColumn(name="parentitemuuid", referencedColumnName="uuid") + private List subBookingItems; + + @OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true) + @JoinColumn(name="bookingitemuuid", referencedColumnName="uuid") + private List subHostingAssets; + @Transient private PatchableMapWrapper resourcesWrapper; @@ -150,16 +161,6 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab return parentItem == null ? null : parentItem.relatedProject(); } - @Override - public String getPropertiesName() { - return "resources"; - } - - @Override - public Map getProperties() { - return resources; - } - public HsBookingProjectEntity getRelatedProject() { return project != null ? project : parentItem.getRelatedProject(); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java new file mode 100644 index 00000000..7d002bac --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java @@ -0,0 +1,77 @@ +package net.hostsharing.hsadminng.hs.booking.item.validators; + +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; +import net.hostsharing.hsadminng.hs.validation.ValidatableProperty; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import static java.util.Arrays.stream; +import static java.util.Collections.emptyList; +import static java.util.Optional.ofNullable; + +public class HsBookingItemEntityValidator extends HsEntityValidator { + + public HsBookingItemEntityValidator(final ValidatableProperty... properties) { + super(properties); + } + + public List validate(final HsBookingItemEntity bookingItem) { + return sequentiallyValidate( + () -> validateProperties(bookingItem), + () -> optionallyValidate(bookingItem.getParentItem()), + () -> validateAgainstSubEntities(bookingItem) + ); + } + + private List validateProperties(final HsBookingItemEntity bookingItem) { + return enrich(prefix(bookingItem.toShortString(), "resources"), validateProperties(bookingItem.getResources())); + } + + private static List optionallyValidate(final HsBookingItemEntity bookingItem) { + return bookingItem != null + ? enrich(prefix(bookingItem.toShortString(), ""), + HsBookingItemEntityValidatorRegistry.doValidate(bookingItem)) + : emptyList(); + } + + protected List validateAgainstSubEntities(final HsBookingItemEntity bookingItem) { + return enrich(prefix(bookingItem.toShortString(), "resources"), + Stream.concat( + stream(propertyValidators) + .map(propDef -> propDef.validateTotals(bookingItem)) + .flatMap(Collection::stream), + stream(propertyValidators) + .filter(ValidatableProperty::isTotalsValidator) + .map(prop -> validateMaxTotalValue(bookingItem, prop)) + ).filter(Objects::nonNull).toList()); + } + + // TODO.refa: convert into generic shape like multi-options validator + private static String validateMaxTotalValue( + final HsBookingItemEntity bookingItem, + final ValidatableProperty propDef) { + final var propName = propDef.propertyName(); + final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse(""); + final var totalValue = ofNullable(bookingItem.getSubBookingItems()).orElse(emptyList()) + .stream() + .map(subItem -> propDef.getValue(subItem.getResources())) + .map(HsBookingItemEntityValidator::toNonNullInteger) + .reduce(0, Integer::sum); + final var maxValue = getNonNullIntegerValue(propDef, bookingItem.getResources()); + if (propDef.thresholdPercentage() != null ) { + return totalValue > (maxValue * propDef.thresholdPercentage() / 100) + ? "%s' maximum total is %d%s, but actual total %s %d%s, which exceeds threshold of %d%%" + .formatted(propName, maxValue, propUnit, propName, totalValue, propUnit, propDef.thresholdPercentage()) + : null; + } else { + return totalValue > maxValue + ? "%s' maximum total is %d%s, but actual total %s %d%s" + .formatted(propName, maxValue, propUnit, propName, totalValue, propUnit) + : null; + } + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidators.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java similarity index 56% rename from src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidators.java rename to src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java index 1f4493e2..e067781e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidators.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java @@ -1,12 +1,12 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; -import lombok.experimental.UtilityClass; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; +import net.hostsharing.hsadminng.errors.MultiValidationException; -import jakarta.validation.ValidationException; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; @@ -14,37 +14,42 @@ import static java.util.Arrays.stream; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.PRIVATE_CLOUD; -@UtilityClass -public class HsBookingItemEntityValidators { +public class HsBookingItemEntityValidatorRegistry { - private static final Map, HsEntityValidator> validators = new HashMap<>(); + private static final Map, HsEntityValidator> validators = new HashMap<>(); static { + register(PRIVATE_CLOUD, new HsPrivateCloudBookingItemValidator()); register(CLOUD_SERVER, new HsCloudServerBookingItemValidator()); register(MANAGED_SERVER, new HsManagedServerBookingItemValidator()); register(MANAGED_WEBSPACE, new HsManagedWebspaceBookingItemValidator()); } - private static void register(final Enum type, final HsEntityValidator validator) { + private static void register(final Enum type, final HsEntityValidator validator) { stream(validator.propertyValidators).forEach( entry -> { entry.verifyConsistency(Map.entry(type, validator)); }); validators.put(type, validator); } - public static HsEntityValidator forType(final Enum type) { - return validators.get(type); + public static HsEntityValidator forType(final Enum type) { + if ( validators.containsKey(type)) { + return validators.get(type); + } + throw new IllegalArgumentException("no validator found for type " + type); } public static Set> types() { return validators.keySet(); } - public static HsBookingItemEntity valid(final HsBookingItemEntity entityToSave) { - final var violations = HsBookingItemEntityValidators.forType(entityToSave.getType()).validate(entityToSave); - if (!violations.isEmpty()) { - throw new ValidationException(violations.toString()); - } + public static List doValidate(final HsBookingItemEntity bookingItem) { + return HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validate(bookingItem); + } + + public static HsBookingItemEntity validated(final HsBookingItemEntity entityToSave) { + MultiValidationException.throwInvalid(doValidate(entityToSave)); return entityToSave; } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidator.java index fa09f2c3..07bb80da 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidator.java @@ -1,20 +1,18 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; -import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; -import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty; -import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty; -class HsCloudServerBookingItemValidator extends HsEntityValidator { +import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty; +import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; + +class HsCloudServerBookingItemValidator extends HsBookingItemEntityValidator { HsCloudServerBookingItemValidator() { super( integerProperty("CPUs").min(1).max(32).required(), integerProperty("RAM").unit("GB").min(1).max(128).required(), integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(), - integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(), + integerProperty("HDD").unit("GB").min(0).max(4000).step(250).withDefault(0), integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(), enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional() ); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidator.java index 79c41070..a267b104 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidator.java @@ -1,24 +1,22 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; -import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; -import static net.hostsharing.hsadminng.hs.validation.BooleanPropertyValidator.booleanProperty; -import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty; -import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty; -class HsManagedServerBookingItemValidator extends HsEntityValidator { +import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty; +import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty; +import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; + +class HsManagedServerBookingItemValidator extends HsBookingItemEntityValidator { HsManagedServerBookingItemValidator() { super( integerProperty("CPUs").min(1).max(32).required(), integerProperty("RAM").unit("GB").min(1).max(128).required(), - integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(), - integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(), - integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(), - enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional(), - booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").optional(), + integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required().asTotalLimit().withThreshold(200), + integerProperty("HDD").unit("GB").min(0).max(4000).step(250).withDefault(0).asTotalLimit().withThreshold(200), + integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required().asTotalLimit().withThreshold(200), + enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").withDefault("BASIC"), + booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").withDefault(false), booleanProperty("SLA-Maria").falseIf("SLA-Platform", "BASIC").optional(), booleanProperty("SLA-PgSQL").falseIf("SLA-Platform", "BASIC").optional(), booleanProperty("SLA-Office").falseIf("SLA-Platform", "BASIC").optional(), diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java index 482d0900..bf637f15 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java @@ -1,24 +1,98 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; -import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; +import net.hostsharing.hsadminng.hs.validation.IntegerProperty; +import org.apache.commons.lang3.function.TriFunction; +import java.util.List; -import static net.hostsharing.hsadminng.hs.validation.BooleanPropertyValidator.booleanProperty; -import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty; -import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty; +import static java.util.Collections.emptyList; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_EMAIL_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.EMAIL_ADDRESS; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_DATABASE; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_USER; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.PGSQL_DATABASE; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.PGSQL_USER; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER; +import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty; +import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty; +import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; -class HsManagedWebspaceBookingItemValidator extends HsEntityValidator { +class HsManagedWebspaceBookingItemValidator extends HsBookingItemEntityValidator { public HsManagedWebspaceBookingItemValidator() { super( integerProperty("SSD").unit("GB").min(1).max(100).step(1).required(), integerProperty("HDD").unit("GB").min(0).max(250).step(10).optional(), integerProperty("Traffic").unit("GB").min(10).max(1000).step(10).required(), - enumerationProperty("SLA-Platform").values("BASIC", "EXT24H").optional(), - integerProperty("Daemons").min(0).max(10).optional(), - booleanProperty("Online Office Server").optional() + integerProperty("Multi").min(1).max(100).step(1).withDefault(1) + .eachComprising( 25, unixUsers()) + .eachComprising( 5, databaseUsers()) + .eachComprising( 5, databases()) + .eachComprising(250, eMailAddresses()), + integerProperty("Daemons").min(0).max(10).withDefault(0), + booleanProperty("Online Office Server").optional(), + enumerationProperty("SLA-Platform").values("BASIC", "EXT24H").withDefault("BASIC") ); } + + private static TriFunction> unixUsers() { + return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> { + final var unixUserCount = entity.getSubHostingAssets().stream() + .flatMap(ha -> ha.getSubHostingAssets().stream()) + .filter(ha -> ha.getType() == UNIX_USER) + .count(); + final long limitingValue = prop.getValue(entity.getResources()); + if (unixUserCount > factor*limitingValue) { + return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " unix users, but " + unixUserCount + " found"); + } + return emptyList(); + }; + } + + private static TriFunction> databaseUsers() { + return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> { + final var unixUserCount = entity.getSubHostingAssets().stream() + .flatMap(ha -> ha.getSubHostingAssets().stream()) + .filter(bi -> bi.getType() == PGSQL_USER || bi.getType() == MARIADB_USER ) + .count(); + final long limitingValue = prop.getValue(entity.getResources()); + if (unixUserCount > factor*limitingValue) { + return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " database users, but " + unixUserCount + " found"); + } + return emptyList(); + }; + } + + private static TriFunction> databases() { + return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> { + final var unixUserCount = entity.getSubHostingAssets().stream() + .flatMap(ha -> ha.getSubHostingAssets().stream()) + .filter(bi -> bi.getType()==PGSQL_USER || bi.getType()==MARIADB_USER ) + .flatMap(domainEMailSetup -> domainEMailSetup.getSubHostingAssets().stream() + .filter(ha -> ha.getType()==PGSQL_DATABASE || ha.getType()==MARIADB_DATABASE)) + .count(); + final long limitingValue = prop.getValue(entity.getResources()); + if (unixUserCount > factor*limitingValue) { + return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " databases, but " + unixUserCount + " found"); + } + return emptyList(); + }; + } + + private static TriFunction> eMailAddresses() { + return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> { + final var unixUserCount = entity.getSubHostingAssets().stream() + .flatMap(ha -> ha.getSubHostingAssets().stream()) + .filter(bi -> bi.getType() == DOMAIN_EMAIL_SETUP) + .flatMap(domainEMailSetup -> domainEMailSetup.getSubHostingAssets().stream() + .filter(ha -> ha.getType()==EMAIL_ADDRESS)) + .count(); + final long limitingValue = prop.getValue(entity.getResources()); + if (unixUserCount > factor*limitingValue) { + return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " databases, but " + unixUserCount + " found"); + } + return emptyList(); + }; + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidator.java new file mode 100644 index 00000000..317f2f0c --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidator.java @@ -0,0 +1,18 @@ +package net.hostsharing.hsadminng.hs.booking.item.validators; + +import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty; +import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; + +class HsPrivateCloudBookingItemValidator extends HsBookingItemEntityValidator { + + HsPrivateCloudBookingItemValidator() { + super( + integerProperty("CPUs").min(4).max(128).required().asTotalLimit(), + integerProperty("RAM").unit("GB").min(4).max(512).required().asTotalLimit(), + integerProperty("SSD").unit("GB").min(100).max(4000).step(25).required().asTotalLimit(), + integerProperty("HDD").unit("GB").min(0).max(16000).step(25).withDefault(0).asTotalLimit(), + integerProperty("Traffic").unit("GB").min(1000).max(40000).step(250).required().asTotalLimit(), + enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").withDefault("BASIC") + ); + } +} 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 a645bb78..76003671 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 @@ -20,7 +20,7 @@ import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; -import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators.valid; +import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry.validated; @RestController public class HsHostingAssetController implements HsHostingAssetsApi { @@ -62,7 +62,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { final var entityToSave = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); - final var saved = assetRepo.save(valid(entityToSave)); + final var saved = validated(assetRepo.save(entityToSave)); final var uri = MvcUriComponentsBuilder.fromController(getClass()) @@ -117,7 +117,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { new HsHostingAssetEntityPatcher(current).apply(body); - final var saved = assetRepo.save(valid(current)); + final var saved = validated(assetRepo.save(current)); final var mapped = mapper.map(saved, HsHostingAssetResource.class); return ResponseEntity.ok(mapped); } 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 8d573c48..3f8202ef 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 @@ -8,7 +8,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; -import net.hostsharing.hsadminng.hs.validation.Validatable; import net.hostsharing.hsadminng.mapper.PatchableMapWrapper; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; @@ -17,6 +16,7 @@ import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; import org.hibernate.annotations.Type; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -25,11 +25,13 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import jakarta.persistence.Transient; import jakarta.persistence.Version; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -56,7 +58,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @Setter @NoArgsConstructor @AllArgsConstructor -public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validatable { +public class HsHostingAssetEntity implements Stringifyable, RbacObject { private static Stringify stringify = stringify(HsHostingAssetEntity.class) .withProp(HsHostingAssetEntity::getType) @@ -91,6 +93,10 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validata @Enumerated(EnumType.STRING) private HsHostingAssetType type; + @OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true) + @JoinColumn(name="parentassetuuid", referencedColumnName="uuid") + private List subHostingAssets; + @Column(name = "identifier") private String identifier; // vm1234, xyz00, example.org, xyz00_abc @@ -114,16 +120,6 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validata PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfg); } - @Override - public String getPropertiesName() { - return "config"; - } - - @Override - public Map getProperties() { - return config; - } - @Override public String toString() { return stringify.apply(this); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java index 47852310..0da530bd 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset; -import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators; +import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetPropsApi; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource; import org.springframework.http.ResponseEntity; @@ -15,7 +15,7 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi { @Override public ResponseEntity> listAssetTypes() { - final var resource = HsHostingAssetEntityValidators.types().stream() + final var resource = HsHostingAssetEntityValidatorRegistry.types().stream() .map(Enum::name) .toList(); return ResponseEntity.ok(resource); @@ -25,7 +25,8 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi { public ResponseEntity> listAssetTypeProps( final HsHostingAssetTypeResource assetType) { - final var propValidators = HsHostingAssetEntityValidators.forType(HsHostingAssetType.of(assetType)); + final Enum type = HsHostingAssetType.of(assetType); + final var propValidators = HsHostingAssetEntityValidatorRegistry.forType(type); final List> resource = propValidators.properties(); return ResponseEntity.ok(toListOfObjects(resource)); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java new file mode 100644 index 00000000..3a0438ee --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java @@ -0,0 +1,76 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; +import net.hostsharing.hsadminng.hs.validation.ValidatableProperty; + +import java.util.List; +import java.util.Objects; + +import static java.util.Arrays.stream; +import static java.util.Collections.emptyList; +import static java.util.Optional.ofNullable; + +public class HsHostingAssetEntityValidator extends HsEntityValidator { + + public HsHostingAssetEntityValidator(final ValidatableProperty... properties) { + super(properties); + } + + + @Override + public List validate(final HsHostingAssetEntity assetEntity) { + return sequentiallyValidate( + () -> validateProperties(assetEntity), + () -> optionallyValidate(assetEntity.getBookingItem()), + () -> optionallyValidate(assetEntity.getParentAsset()), + () -> validateAgainstSubEntities(assetEntity) + ); + } + + private List validateProperties(final HsHostingAssetEntity assetEntity) { + return enrich(prefix(assetEntity.toShortString(), "config"), validateProperties(assetEntity.getConfig())); + } + + private static List optionallyValidate(final HsHostingAssetEntity assetEntity) { + return assetEntity != null + ? enrich(prefix(assetEntity.toShortString(), "parentAsset"), + HsHostingAssetEntityValidatorRegistry.forType(assetEntity.getType()).validate(assetEntity)) + : emptyList(); + } + + private static List optionallyValidate(final HsBookingItemEntity bookingItem) { + return bookingItem != null + ? enrich(prefix(bookingItem.toShortString(), "bookingItem"), + HsBookingItemEntityValidatorRegistry.doValidate(bookingItem)) + : emptyList(); + } + + protected List validateAgainstSubEntities(final HsHostingAssetEntity assetEntity) { + return enrich(prefix(assetEntity.toShortString(), "config"), + stream(propertyValidators) + .filter(ValidatableProperty::isTotalsValidator) + .map(prop -> validateMaxTotalValue(assetEntity, prop)) + .filter(Objects::nonNull) + .toList()); + } + + private String validateMaxTotalValue( + final HsHostingAssetEntity hostingAsset, + final ValidatableProperty propDef) { + final var propName = propDef.propertyName(); + final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse(""); + final var totalValue = ofNullable(hostingAsset.getSubHostingAssets()).orElse(emptyList()) + .stream() + .map(subItem -> propDef.getValue(subItem.getConfig())) + .map(HsEntityValidator::toNonNullInteger) + .reduce(0, Integer::sum); + final var maxValue = getNonNullIntegerValue(propDef, hostingAsset.getConfig()); + return totalValue > maxValue + ? "%s' maximum total is %d%s, but actual total is %s %d%s".formatted( + propName, maxValue, propUnit, propName, totalValue, propUnit) + : null; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java new file mode 100644 index 00000000..a1cac8e0 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java @@ -0,0 +1,50 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; +import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; +import net.hostsharing.hsadminng.errors.MultiValidationException; + +import java.util.*; + +import static java.util.Arrays.stream; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.*; + +public class HsHostingAssetEntityValidatorRegistry { + + private static final Map, HsEntityValidator> validators = new HashMap<>(); + static { + register(CLOUD_SERVER, new HsHostingAssetEntityValidator()); + register(MANAGED_SERVER, new HsManagedServerHostingAssetValidator()); + register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator()); + register(UNIX_USER, new HsHostingAssetEntityValidator()); + } + + private static void register(final Enum type, final HsEntityValidator validator) { + stream(validator.propertyValidators).forEach( entry -> { + entry.verifyConsistency(Map.entry(type, validator)); + }); + validators.put(type, validator); + } + + public static HsEntityValidator forType(final Enum type) { + if ( validators.containsKey(type)) { + return validators.get(type); + } + throw new IllegalArgumentException("no validator found for type " + type); + } + + public static Set> types() { + return validators.keySet(); + } + + public static List doValidate(final HsHostingAssetEntity hostingAsset) { + return HsHostingAssetEntityValidatorRegistry.forType(hostingAsset.getType()).validate(hostingAsset); + } + + public static HsHostingAssetEntity validated(final HsHostingAssetEntity entityToSave) { + MultiValidationException.throwInvalid(doValidate(entityToSave)); + return entityToSave; + } + +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidators.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidators.java deleted file mode 100644 index 11df9a84..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidators.java +++ /dev/null @@ -1,51 +0,0 @@ -package net.hostsharing.hsadminng.hs.hosting.asset.validators; - -import lombok.experimental.UtilityClass; -import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; -import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; -import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; - -import jakarta.validation.ValidationException; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -import static java.util.Arrays.stream; -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; - -@UtilityClass -public class HsHostingAssetEntityValidators { - - private static final Map, HsEntityValidator> validators = new HashMap<>(); - static { - register(CLOUD_SERVER, new HsEntityValidator<>()); - register(MANAGED_SERVER, new HsManagedServerHostingAssetValidator()); - register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator()); - } - - private static void register(final Enum type, final HsEntityValidator validator) { - stream(validator.propertyValidators).forEach( entry -> { - entry.verifyConsistency(Map.entry(type, validator)); - }); - validators.put(type, validator); - } - - public static HsEntityValidator forType(final Enum type) { - return validators.get(type); - } - - public static Set> types() { - return validators.keySet(); - } - - - public static HsHostingAssetEntity valid(final HsHostingAssetEntity entityToSave) { - final var violations = HsHostingAssetEntityValidators.forType(entityToSave.getType()).validate(entityToSave); - if (!violations.isEmpty()) { - throw new ValidationException(violations.toString()); - } - return entityToSave; - } -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java index 35f3b81d..b2107866 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java @@ -1,12 +1,8 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; -import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; -import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; -import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; +import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; -import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty; - -class HsManagedServerHostingAssetValidator extends HsEntityValidator { +class HsManagedServerHostingAssetValidator extends HsHostingAssetEntityValidator { public HsManagedServerHostingAssetValidator() { super( 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 ffef39d7..19c9dc24 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 @@ -1,28 +1,29 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; -import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; -import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; +import java.util.Collection; +import java.util.stream.Stream; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; - -class HsManagedWebspaceHostingAssetValidator extends HsEntityValidator { +class HsManagedWebspaceHostingAssetValidator extends HsHostingAssetEntityValidator { public HsManagedWebspaceHostingAssetValidator() { } @Override public List validate(final HsHostingAssetEntity assetEntity) { - final var result = super.validate(assetEntity); - validateIdentifierPattern(result, assetEntity); - - return result; + return Stream.of(validateIdentifierPattern(assetEntity), super.validate(assetEntity)) + .flatMap(Collection::stream) + .collect(Collectors.toList()); } - private static void validateIdentifierPattern(final List result, final HsHostingAssetEntity assetEntity) { + private static List validateIdentifierPattern(final HsHostingAssetEntity assetEntity) { 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() + "'"); + return List.of("'identifier' expected to match '"+expectedIdentifierPattern+"', but is '" + assetEntity.getIdentifier() + "'"); } + return Collections.emptyList(); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java index a22065c0..6279ad05 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.office.coopassets; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopAssetsApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*; +import net.hostsharing.hsadminng.errors.MultiValidationException; import net.hostsharing.hsadminng.mapper.Mapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.format.annotation.DateTimeFormat; @@ -13,14 +14,12 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import jakarta.persistence.EntityNotFoundException; -import jakarta.validation.ValidationException; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; -import static java.lang.String.join; import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.*; @RestController @@ -97,9 +96,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse validateDebitTransaction(requestBody, violations); validateCreditTransaction(requestBody, violations); validateAssetValue(requestBody, violations); - if (violations.size() > 0) { - throw new ValidationException("[" + join(", ", violations) + "]"); - } + MultiValidationException.throwInvalid(violations); } private static void validateDebitTransaction( diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java index 9a3295a2..f90d5276 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java @@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopSharesApi; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionInsertResource; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionResource; +import net.hostsharing.hsadminng.errors.MultiValidationException; import net.hostsharing.hsadminng.mapper.Mapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.format.annotation.DateTimeFormat; @@ -14,14 +15,12 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; -import jakarta.validation.ValidationException; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; -import static java.lang.String.join; import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.CANCELLATION; import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.SUBSCRIPTION; @@ -99,9 +98,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar validateSubscriptionTransaction(requestBody, violations); validateCancellationTransaction(requestBody, violations); validateshareCount(requestBody, violations); - if (violations.size() > 0) { - throw new ValidationException("[" + join(", ", violations) + "]"); - } + MultiValidationException.throwInvalid(violations); } private static void validateSubscriptionTransaction( diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java new file mode 100644 index 00000000..9d664683 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java @@ -0,0 +1,46 @@ +package net.hostsharing.hsadminng.hs.validation; + +import lombok.Setter; +import net.hostsharing.hsadminng.mapper.Array; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Map; +import java.util.Objects; + +@Setter +public class BooleanProperty extends ValidatableProperty { + + private static final String[] KEY_ORDER = Array.join(ValidatableProperty.KEY_ORDER_HEAD, ValidatableProperty.KEY_ORDER_TAIL); + + private Map.Entry falseIf; + + private BooleanProperty(final String propertyName) { + super(Boolean.class, propertyName, KEY_ORDER); + } + + public static BooleanProperty booleanProperty(final String propertyName) { + return new BooleanProperty(propertyName); + } + + public ValidatableProperty falseIf(final String refPropertyName, final String refPropertyValue) { + this.falseIf = new AbstractMap.SimpleImmutableEntry<>(refPropertyName, refPropertyValue); + return this; + } + + @Override + protected void validate(final ArrayList result, final Boolean propValue, final Map props) { + if (falseIf != null && propValue) { + final Object referencedValue = props.get(falseIf.getKey()); + if (Objects.equals(referencedValue, falseIf.getValue())) { + result.add(propertyName + "' is expected to be false because " + + falseIf.getKey() + "=" + referencedValue + " but is " + propValue); + } + } + } + + @Override + protected String simpleTypeName() { + return "boolean"; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanPropertyValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanPropertyValidator.java deleted file mode 100644 index 2838e0f5..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanPropertyValidator.java +++ /dev/null @@ -1,42 +0,0 @@ -package net.hostsharing.hsadminng.hs.validation; - -import lombok.Setter; - -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.Map; -import java.util.Objects; - -@Setter -public class BooleanPropertyValidator extends HsPropertyValidator { - - private Map.Entry falseIf; - - private BooleanPropertyValidator(final String propertyName) { - super(Boolean.class, propertyName); - } - - public static BooleanPropertyValidator booleanProperty(final String propertyName) { - return new BooleanPropertyValidator(propertyName); - } - - public HsPropertyValidator falseIf(final String refPropertyName, final String refPropertyValue) { - this.falseIf = new AbstractMap.SimpleImmutableEntry<>(refPropertyName, refPropertyValue); - return this; - } - - @Override - protected void validate(final ArrayList result, final String propertiesName, final Boolean propValue, final Map props) { - if (falseIf != null && !Objects.equals(props.get(falseIf.getKey()), falseIf.getValue())) { - if (propValue) { - result.add("'"+propertiesName+"." + propertyName + "' is expected to be false because " + - propertiesName+"." + falseIf.getKey()+ "=" + falseIf.getValue() + " but is " + propValue); - } - } - } - - @Override - protected String simpleTypeName() { - return "boolean"; - } -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java new file mode 100644 index 00000000..23e5ef61 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java @@ -0,0 +1,44 @@ +package net.hostsharing.hsadminng.hs.validation; + +import lombok.Setter; +import net.hostsharing.hsadminng.mapper.Array; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; + +@Setter +public class EnumerationProperty extends ValidatableProperty { + + private static final String[] KEY_ORDER = Array.join( + ValidatableProperty.KEY_ORDER_HEAD, + Array.of("values"), + ValidatableProperty.KEY_ORDER_TAIL); + + private String[] values; + + private EnumerationProperty(final String propertyName) { + super(String.class, propertyName, KEY_ORDER); + } + + public static EnumerationProperty enumerationProperty(final String propertyName) { + return new EnumerationProperty(propertyName); + } + + public ValidatableProperty values(final String... values) { + this.values = values; + return this; + } + + @Override + protected void validate(final ArrayList result, final String propValue, final Map props) { + if (Arrays.stream(values).noneMatch(v -> v.equals(propValue))) { + result.add(propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'"); + } + } + + @Override + protected String simpleTypeName() { + return "enumeration"; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationPropertyValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationPropertyValidator.java deleted file mode 100644 index 329feb74..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationPropertyValidator.java +++ /dev/null @@ -1,38 +0,0 @@ -package net.hostsharing.hsadminng.hs.validation; - -import lombok.Setter; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Map; - -@Setter -public class EnumerationPropertyValidator extends HsPropertyValidator { - - private String[] values; - - private EnumerationPropertyValidator(final String propertyName) { - super(String.class, propertyName); - } - - public static EnumerationPropertyValidator enumerationProperty(final String propertyName) { - return new EnumerationPropertyValidator(propertyName); - } - - public HsPropertyValidator values(final String... values) { - this.values = values; - return this; - } - - @Override - protected void validate(final ArrayList result, final String propertiesName, final String propValue, final Map props) { - if (Arrays.stream(values).noneMatch(v -> v.equals(propValue))) { - result.add("'"+propertiesName+"." + propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'"); - } - } - - @Override - protected String simpleTypeName() { - return "enumeration"; - } -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java index 43be4d10..c06ed140 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java @@ -1,49 +1,76 @@ package net.hostsharing.hsadminng.hs.validation; -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.databind.ObjectMapper; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import static java.util.Arrays.stream; +import static java.util.Collections.emptyList; -public class HsEntityValidator, T extends Enum> { +public abstract class HsEntityValidator { - public final HsPropertyValidator[] propertyValidators; + public final ValidatableProperty[] propertyValidators; - public HsEntityValidator(final HsPropertyValidator... validators) { + public HsEntityValidator(final ValidatableProperty... validators) { propertyValidators = validators; } - public List validate(final E assetEntity) { + protected static List enrich(final String prefix, final List messages) { + return messages.stream() + // TODO:refa: this is a bit hacky, I need to find the right place to add the prefix + .map(message -> message.startsWith("'") ? message : ("'" + prefix + "." + message)) + .toList(); + } + + protected static String prefix(final String... parts) { + return String.join(".", parts); + } + + public abstract List validate(final E entity); + + public final List> properties() { + return Arrays.stream(propertyValidators) + .map(ValidatableProperty::toOrderedMap) + .toList(); + } + + protected ArrayList validateProperties(final Map properties) { final var result = new ArrayList(); - assetEntity.getProperties().keySet().forEach( givenPropName -> { + properties.keySet().forEach( givenPropName -> { if (stream(propertyValidators).map(pv -> pv.propertyName).noneMatch(propName -> propName.equals(givenPropName))) { - result.add("'"+assetEntity.getPropertiesName()+"." + givenPropName + "' is not expected but is set to '" +assetEntity.getProperties().get(givenPropName) + "'"); + result.add(givenPropName + "' is not expected but is set to '" + properties.get(givenPropName) + "'"); } }); stream(propertyValidators).forEach(pv -> { - result.addAll(pv.validate(assetEntity.getPropertiesName(), assetEntity.getProperties())); + result.addAll(pv.validate(properties)); }); return result; } - public List> properties() { - final var mapper = new ObjectMapper(); - mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); - return Arrays.stream(propertyValidators) - .map(propertyValidator -> propertyValidator.toMap(mapper)) - .map(HsEntityValidator::asKeyValueMap) - .toList(); + @SafeVarargs + protected static List sequentiallyValidate(final Supplier>... validators) { + return new ArrayList<>(stream(validators) + .map(Supplier::get) + .filter(violations -> !violations.isEmpty()) + .findFirst() + .orElse(emptyList())); } - @SuppressWarnings({ "unchecked", "rawtypes" }) - private static Map asKeyValueMap(final Map map) { - return (Map) map; + protected static Integer getNonNullIntegerValue(final ValidatableProperty prop, final Map propValues) { + final var value = prop.getValue(propValues); + if (value instanceof Integer) { + return (Integer) value; + } + throw new IllegalArgumentException(prop.propertyName + " Integer value expected, but got " + value); } + protected static Integer toNonNullInteger(final Object value) { + if (value instanceof Integer) { + return (Integer) value; + } + throw new IllegalArgumentException("Integer value expected, but got " + value); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsPropertyValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsPropertyValidator.java deleted file mode 100644 index 891c8a7a..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsPropertyValidator.java +++ /dev/null @@ -1,67 +0,0 @@ -package net.hostsharing.hsadminng.hs.validation; - -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; - -import java.util.AbstractMap.SimpleImmutableEntry; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -@RequiredArgsConstructor -public abstract class HsPropertyValidator { - - final Class type; - final String propertyName; - private Boolean required; - - public static Map.Entry defType(K k, V v) { - return new SimpleImmutableEntry<>(k, v); - } - - public HsPropertyValidator required() { - required = Boolean.TRUE; - return this; - } - - public HsPropertyValidator optional() { - required = Boolean.FALSE; - return this; - } - - public final List validate(final String propertiesName, final Map props) { - final var result = new ArrayList(); - final var propValue = props.get(propertyName); - if (propValue == null) { - if (required) { - result.add("'"+propertiesName+"." + propertyName + "' is required but missing"); - } - } - if (propValue != null){ - if ( type.isInstance(propValue)) { - //noinspection unchecked - validate(result, propertiesName, (T) propValue, props); - } else { - result.add("'"+propertiesName+"." + propertyName + "' is expected to be of type " + type + ", " + - "but is of type '" + propValue.getClass().getSimpleName() + "'"); - } - } - return result; - } - - protected abstract void validate(final ArrayList result, final String propertiesName, final T propValue, final Map props); - - public void verifyConsistency(final Map.Entry, ?> typeDef) { - if (required == null ) { - throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required() or .optional()" ); - } - } - - public Map toMap(final ObjectMapper mapper) { - final Map map = mapper.convertValue(this, Map.class); - map.put("type", simpleTypeName()); - return map; - } - - protected abstract String simpleTypeName(); -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java new file mode 100644 index 00000000..a1658ff9 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java @@ -0,0 +1,56 @@ +package net.hostsharing.hsadminng.hs.validation; + +import lombok.Setter; +import net.hostsharing.hsadminng.mapper.Array; + +import java.util.ArrayList; +import java.util.Map; + +@Setter +public class IntegerProperty extends ValidatableProperty { + + private final static String[] KEY_ORDER = Array.join( + ValidatableProperty.KEY_ORDER_HEAD, + Array.of("unit", "min", "max", "step"), + ValidatableProperty.KEY_ORDER_TAIL); + + private String unit; + private Integer min; + private Integer max; + private Integer step; + + public static IntegerProperty integerProperty(final String propertyName) { + return new IntegerProperty(propertyName); + } + + private IntegerProperty(final String propertyName) { + super(Integer.class, propertyName, KEY_ORDER); + } + + @Override + public String unit() { + return unit; + } + + public Integer max() { + return max; + } + + @Override + protected void validate(final ArrayList result, final Integer propValue, final Map props) { + if (min != null && propValue < min) { + result.add(propertyName + "' is expected to be >= " + min + " but is " + propValue); + } + if (max != null && propValue > max) { + result.add(propertyName + "' is expected to be <= " + max + " but is " + propValue); + } + if (step != null && propValue % step != 0) { + result.add(propertyName + "' is expected to be multiple of " + step + " but is " + propValue); + } + } + + @Override + protected String simpleTypeName() { + return "integer"; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerPropertyValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerPropertyValidator.java deleted file mode 100644 index d6fb85f5..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerPropertyValidator.java +++ /dev/null @@ -1,42 +0,0 @@ -package net.hostsharing.hsadminng.hs.validation; - -import lombok.Setter; - -import java.util.ArrayList; -import java.util.Map; - -@Setter -public class IntegerPropertyValidator extends HsPropertyValidator { - - private String unit; - private Integer min; - private Integer max; - private Integer step; - - public static IntegerPropertyValidator integerProperty(final String propertyName) { - return new IntegerPropertyValidator(propertyName); - } - - private IntegerPropertyValidator(final String propertyName) { - super(Integer.class, propertyName); - } - - - @Override - protected void validate(final ArrayList result, final String propertiesName, final Integer propValue, final Map props) { - if (min != null && propValue < min) { - result.add("'"+propertiesName+"." + propertyName + "' is expected to be >= " + min + " but is " + propValue); - } - if (max != null && propValue > max) { - result.add("'"+propertiesName+"." + propertyName + "' is expected to be <= " + max + " but is " + propValue); - } - if (step != null && propValue % step != 0) { - result.add("'"+propertiesName+"." + propertyName + "' is expected to be multiple of " + step + " but is " + propValue); - } - } - - @Override - protected String simpleTypeName() { - return "integer"; - } -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/Validatable.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/Validatable.java deleted file mode 100644 index 6f214b04..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/Validatable.java +++ /dev/null @@ -1,13 +0,0 @@ -package net.hostsharing.hsadminng.hs.validation; - - -import java.util.Map; - -public interface Validatable> { - - - Enum getType(); - - String getPropertiesName(); - Map getProperties(); -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java new file mode 100644 index 00000000..7795d47d --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java @@ -0,0 +1,172 @@ +package net.hostsharing.hsadminng.hs.validation; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.mapper.Array; +import org.apache.commons.lang3.function.TriFunction; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static java.util.Collections.emptyList; + +@RequiredArgsConstructor +public abstract class ValidatableProperty { + + protected static final String[] KEY_ORDER_HEAD = Array.of("propertyName"); + protected static final String[] KEY_ORDER_TAIL = Array.of("required", "defaultValue", "isTotalsValidator", "thresholdPercentage"); + + final Class type; + final String propertyName; + private final String[] keyOrder; + private Boolean required; + private T defaultValue; + private boolean isTotalsValidator = false; + @JsonIgnore + private List>> asTotalLimitValidators; // TODO.impl: move to BookingItemIntegerProperty + + private Integer thresholdPercentage; // TODO.impl: move to IntegerProperty + + public String unit() { + return null; + } + + public ValidatableProperty required() { + required = TRUE; + return this; + } + + public ValidatableProperty optional() { + required = FALSE; + return this; + } + + public ValidatableProperty withDefault(final T value) { + defaultValue = value; + required = FALSE; + return this; + } + + public ValidatableProperty asTotalLimit() { + isTotalsValidator = true; + return this; + } + + public String propertyName() { + return propertyName; + } + + public boolean isTotalsValidator() { + return isTotalsValidator || asTotalLimitValidators != null; + } + + public Integer thresholdPercentage() { + return thresholdPercentage; + } + + public ValidatableProperty eachComprising(final int factor, final TriFunction> validator) { + if (asTotalLimitValidators == null) { + asTotalLimitValidators = new ArrayList<>(); + } + asTotalLimitValidators.add((final HsBookingItemEntity entity) -> validator.apply(entity, (IntegerProperty)this, factor)); + return this; + } + + public ValidatableProperty withThreshold(final Integer percentage) { + this.thresholdPercentage = percentage; + return this; + } + + public final List validate(final Map props) { + final var result = new ArrayList(); + final var propValue = props.get(propertyName); + if (propValue == null) { + if (required) { + result.add(propertyName + "' is required but missing"); + } + } + if (propValue != null){ + if ( type.isInstance(propValue)) { + //noinspection unchecked + validate(result, (T) propValue, props); + } else { + result.add(propertyName + "' is expected to be of type " + type + ", " + + "but is of type '" + propValue.getClass().getSimpleName() + "'"); + } + } + return result; + } + + protected abstract void validate(final ArrayList result, final T propValue, final Map props); + + public void verifyConsistency(final Map.Entry, ?> typeDef) { + if (required == null ) { + throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required() or .optional()" ); + } + } + + @SuppressWarnings("unchecked") + public T getValue(final Map propValues) { + return (T) Optional.ofNullable(propValues.get(propertyName)).orElse(defaultValue); + } + + protected abstract String simpleTypeName(); + + public Map toOrderedMap() { + Map sortedMap = new LinkedHashMap<>(); + sortedMap.put("type", simpleTypeName()); + + // Add entries according to the given order + for (String key : keyOrder) { + final Optional propValue = getPropertyValue(key); + propValue.ifPresent(o -> sortedMap.put(key, o)); + } + + return sortedMap; + } + + @SneakyThrows + private Optional getPropertyValue(final String key) { + try { + final var field = getClass().getDeclaredField(key); + field.setAccessible(true); + return Optional.ofNullable(arrayToList(field.get(this))); + } catch (final NoSuchFieldException e1) { + try { + final var field = getClass().getSuperclass().getDeclaredField(key); + field.setAccessible(true); + return Optional.ofNullable(arrayToList(field.get(this))); + } catch (final NoSuchFieldException e2) { + return Optional.empty(); + } + } + } + + private Object arrayToList(final Object value) { + if ( value instanceof String[]) { + return List.of((String[])value); + } + return value; + } + + public List validateTotals(final HsBookingItemEntity bookingItem) { + if (asTotalLimitValidators==null) { + return emptyList(); + } + return asTotalLimitValidators.stream() + .map(v -> v.apply(bookingItem)) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .toList(); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/Array.java b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java similarity index 83% rename from src/test/java/net/hostsharing/hsadminng/rbac/test/Array.java rename to src/main/java/net/hostsharing/hsadminng/mapper/Array.java index c51a69bb..39588f11 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/Array.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java @@ -1,4 +1,4 @@ -package net.hostsharing.hsadminng.rbac.test; +package net.hostsharing.hsadminng.mapper; import java.util.ArrayList; import java.util.Arrays; @@ -37,4 +37,10 @@ public class Array { return resultList.toArray(String[]::new); } + public static String[] join(final String[]... parts) { + final String[] joined = Arrays.stream(parts) + .flatMap(Arrays::stream) + .toArray(String[]::new); + return joined; + } } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java index 2290c948..fd33f358 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java @@ -62,6 +62,8 @@ public class RbacGrantsDiagramService { @PersistenceContext private EntityManager em; + private Map> descendantsByUuid = new HashMap<>(); + public String allGrantsToCurrentUser(final EnumSet includes) { final var graph = new LimitedHashSet(); for ( UUID subjectUuid: context.currentSubjectsUuids() ) { @@ -102,7 +104,7 @@ public class RbacGrantsDiagramService { } private void traverseGrantsFrom(final Set graph, final UUID refUuid, final EnumSet option) { - final var grants = rawGrantRepo.findByDescendantUuid(refUuid); + final var grants = findDescendantsByUuid(refUuid); grants.forEach(g -> { if (!option.contains(USERS) && g.getAscendantIdName().startsWith("user:")) { return; @@ -114,6 +116,11 @@ public class RbacGrantsDiagramService { }); } + private List findDescendantsByUuid(final UUID refUuid) { + // TODO.impl: if that UUID already got processed, do we need to return anything at all? + return descendantsByUuid.computeIfAbsent(refUuid, uuid -> rawGrantRepo.findByDescendantUuid(uuid)); + } + private String toMermaidFlowchart(final HashSet graph, final EnumSet includes) { final var entities = includes.contains(DETAILS) diff --git a/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6208-hs-booking-item-test-data.sql b/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6208-hs-booking-item-test-data.sql index bc3a9e51..3f007ab8 100644 --- a/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6208-hs-booking-item-test-data.sql +++ b/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6208-hs-booking-item-test-data.sql @@ -33,13 +33,13 @@ begin 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); + values (privateCloudUuid, relatedProject.uuid, 'PRIVATE_CLOUD', null, 'some PrivateCloud', daterange('20240401', null, '[]'), '{ "CPUs": 10, "RAM": 32, "SSD": 4000, "HDD": 10000, "Traffic": 2000 }'::jsonb), + (uuid_generate_v4(), null, 'MANAGED_SERVER', privateCloudUuid, 'some ManagedServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 2, "RAM": 4, "SSD": 500, "Traffic": 500 }'::jsonb), + (uuid_generate_v4(), null, 'CLOUD_SERVER', privateCloudUuid, 'test CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 2, "RAM": 4, "SSD": 750, "Traffic": 500 }'::jsonb), + (uuid_generate_v4(), null, 'CLOUD_SERVER', privateCloudUuid, 'prod CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 4, "RAM": 16, "SSD": 1000, "Traffic": 500 }'::jsonb), + (managedServerUuid, relatedProject.uuid, 'MANAGED_SERVER', null, 'separate ManagedServer', daterange('20221001', null, '[]'), '{ "CPUs": 2, "RAM": 8, "SSD": 500, "Traffic": 500 }'::jsonb), + (uuid_generate_v4(), null, 'MANAGED_WEBSPACE', managedServerUuid, 'some ManagedWebspace', daterange('20221001', null, '[]'), '{ "SSD": 50, "Traffic": 20, "Daemons": 2, "Multi": 4 }'::jsonb), + (uuid_generate_v4(), relatedProject.uuid, 'MANAGED_WEBSPACE', null, 'separate ManagedWebspace', daterange('20221001', null, '[]'), '{ "SSD": 100, "Traffic": 50, "Daemons": 0, "Multi": 1 }'::jsonb); 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 c6fedb72..7e96a3fd 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 @@ -75,10 +75,10 @@ begin end); if expectedParentType is not null and actualParentType is null then - raise exception '[400] % must have % as parent, but got ', + raise exception '[400] HostingAsset % must have % as parent, but got ', NEW.type, expectedParentType; elsif expectedParentType is not null and actualParentType <> expectedParentType then - raise exception '[400] % must have % as parent, but got %s', + raise exception '[400] HostingAsset % must have % as parent, but got %s', NEW.type, expectedParentType, actualParentType; end if; return NEW; @@ -100,27 +100,23 @@ create or replace function hs_hosting_asset_booking_item_hierarchy_check_tf() language plpgsql as $$ declare actualBookingItemType HsBookingItemType; - expectedBookingItemTypes HsBookingItemType[]; + expectedBookingItemType HsBookingItemType; begin actualBookingItemType := (select type from hs_booking_item where NEW.bookingItemUuid = uuid); if NEW.type = 'CLOUD_SERVER' then - expectedBookingItemTypes := ARRAY['PRIVATE_CLOUD', 'CLOUD_SERVER']; + expectedBookingItemType := 'CLOUD_SERVER'; elsif NEW.type = 'MANAGED_SERVER' then - expectedBookingItemTypes := ARRAY['PRIVATE_CLOUD', 'MANAGED_SERVER']; + expectedBookingItemType := 'MANAGED_SERVER'; elsif NEW.type = 'MANAGED_WEBSPACE' then - if NEW.parentAssetUuid is null then - expectedBookingItemTypes := ARRAY['MANAGED_WEBSPACE']; - else - expectedBookingItemTypes := ARRAY['PRIVATE_CLOUD', 'MANAGED_SERVER']; - end if; + expectedBookingItemType := 'MANAGED_WEBSPACE'; end if; - if not actualBookingItemType = any(expectedBookingItemTypes) then - raise exception '[400] % % must have any of % as booking-item, but got %', - NEW.type, NEW.identifier, expectedBookingItemTypes, actualBookingItemType; + if not actualBookingItemType = expectedBookingItemType then + raise exception '[400] HostingAsset % % must have % as booking-item, but got %', + NEW.type, NEW.identifier, expectedBookingItemType, actualBookingItemType; end if; return NEW; end; $$; 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 964acdec..c82bd768 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 @@ -11,16 +11,18 @@ 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; - debitorNumberSuffix varchar; - defaultPrefix varchar; - managedServerUuid uuid; - managedWebspaceUuid uuid; - webUnixUserUuid uuid; + currentTask varchar; + relatedProject hs_booking_project; + relatedDebitor hs_office_debitor; + relatedPrivateCloudBookingItem hs_booking_item; + relatedManagedServerBookingItem hs_booking_item; + relatedCloudServerBookingItem hs_booking_item; + relatedManagedWebspaceBookingItem hs_booking_item; + debitorNumberSuffix varchar; + defaultPrefix varchar; + managedServerUuid uuid; + managedWebspaceUuid uuid; + webUnixUserUuid uuid; begin currentTask := 'creating hosting-asset test-data ' || givenProjectCaption; call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN'); @@ -38,7 +40,7 @@ begin select item.* into relatedPrivateCloudBookingItem from hs_booking_item item - where item.projectUuid = relatedProject.uuid + where item.projectUuid = relatedProject.uuid and item.type = 'PRIVATE_CLOUD'; assert relatedPrivateCloudBookingItem.uuid is not null, 'relatedPrivateCloudBookingItem for "' || givenProjectCaption|| '" must not be null'; @@ -48,6 +50,18 @@ begin and item.type = 'MANAGED_SERVER'; assert relatedManagedServerBookingItem.uuid is not null, 'relatedManagedServerBookingItem for "' || givenProjectCaption|| '" must not be null'; + select item.* into relatedCloudServerBookingItem + from hs_booking_item item + where item.parentItemuuid = relatedPrivateCloudBookingItem.uuid + and item.type = 'CLOUD_SERVER'; + assert relatedCloudServerBookingItem.uuid is not null, 'relatedCloudServerBookingItem for "' || givenProjectCaption|| '" must not be null'; + + select item.* into relatedManagedWebspaceBookingItem + from hs_booking_item item + where item.projectUuid = relatedProject.uuid + and item.type = 'MANAGED_WEBSPACE'; + assert relatedManagedWebspaceBookingItem.uuid is not null, 'relatedManagedWebspaceBookingItem for "' || givenProjectCaption|| '" must not be null'; + select uuid_generate_v4() into managedServerUuid; select uuid_generate_v4() into managedWebspaceUuid; select uuid_generate_v4() into webUnixUserUuid; @@ -55,12 +69,12 @@ begin defaultPrefix := relatedDebitor.defaultPrefix; insert into hs_hosting_asset - (uuid, bookingitemuuid, type, parentAssetUuid, assignedToAssetUuid, identifier, caption, config) - values (managedServerUuid, relatedPrivateCloudBookingItem.uuid, 'MANAGED_SERVER', null, null, 'vm10' || debitorNumberSuffix, 'some ManagedServer', '{ "extra": 42 }'::jsonb), - (uuid_generate_v4(), relatedPrivateCloudBookingItem.uuid, 'CLOUD_SERVER', null, null, 'vm20' || debitorNumberSuffix, 'another CloudServer', '{ "extra": 42 }'::jsonb), - (managedWebspaceUuid, relatedManagedServerBookingItem.uuid, 'MANAGED_WEBSPACE', managedServerUuid, null, defaultPrefix || '01', 'some Webspace', '{ "extra": 42 }'::jsonb), - (webUnixUserUuid, null, 'UNIX_USER', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some UnixUser for Website', '{ "SSD-soft-quota": "128", "SSD-hard-quota": "256", "HDD-soft-quota": "512", "HDD-hard-quota": "1024", "extra": 42 }'::jsonb), - (uuid_generate_v4(), null, 'DOMAIN_HTTP_SETUP', managedWebspaceUuid, webUnixUserUuid, defaultPrefix || '.example.org', 'some Domain-HTTP-Setup', '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*", "extra": 42 }'::jsonb); + (uuid, bookingitemuuid, type, parentAssetUuid, assignedToAssetUuid, identifier, caption, config) + values (managedServerUuid, relatedManagedServerBookingItem.uuid, 'MANAGED_SERVER', null, null, 'vm10' || debitorNumberSuffix, 'some ManagedServer', '{ "monit_max_cpu_usage": 90, "monit_max_ram_usage": 80, "monit_max_ssd_usage": 70 }'::jsonb), + (uuid_generate_v4(), relatedCloudServerBookingItem.uuid, 'CLOUD_SERVER', null, null, 'vm20' || debitorNumberSuffix, 'another CloudServer', '{}'::jsonb), + (managedWebspaceUuid, relatedManagedWebspaceBookingItem.uuid, 'MANAGED_WEBSPACE', managedServerUuid, null, defaultPrefix || '01', 'some Webspace', '{}'::jsonb), + (webUnixUserUuid, null, 'UNIX_USER', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some UnixUser for Website', '{ "SSD-soft-quota": "128", "SSD-hard-quota": "256", "HDD-soft-quota": "512", "HDD-hard-quota": "1024"}'::jsonb), + (uuid_generate_v4(), null, 'DOMAIN_HTTP_SETUP', managedWebspaceUuid, webUnixUserUuid, defaultPrefix || '.example.org', 'some Domain-HTTP-Setup', '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*"}'::jsonb); end; $$; --// diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index 2c2f9f3d..df26279d 100644 --- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java +++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java @@ -8,7 +8,10 @@ import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.ConditionEvents; import com.tngtech.archunit.lang.SimpleConditionEvent; import net.hostsharing.hsadminng.HsadminNgApplication; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import net.hostsharing.hsadminng.rbac.context.ContextBasedTest; +import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import org.springframework.data.repository.Repository; import org.springframework.web.bind.annotation.RestController; @@ -51,6 +54,7 @@ public class ArchitectureTest { "..hs.office.person", "..hs.office.relation", "..hs.office.sepamandate", + "..hs.booking.debitor", "..hs.booking.project", "..hs.booking.item", "..hs.booking.item.validators", @@ -155,7 +159,8 @@ public class ArchitectureTest { .that().resideInAPackage("..hs.hosting.(*)..") .should().onlyBeAccessed().byClassesThat() .resideInAnyPackage( - "..hs.hosting.(*).." + "..hs.hosting.(*)..", + "..hs.booking.(*).." // TODO.impl: fix this cyclic dependency ); @ArchTest @@ -295,9 +300,13 @@ public class ArchitectureTest { static final ArchRule everythingShouldBeFreeOfCycles = slices().matching("net.hostsharing.hsadminng.(*)..") .should().beFreeOfCycles() + // TODO.refa: would be great if we could get rid of these cyclic dependencies .ignoreDependency( ContextBasedTest.class, - net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService.class); + RbacGrantsDiagramService.class) + .ignoreDependency( + HsBookingItemEntity.class, + HsHostingAssetEntity.class); @ArchTest diff --git a/src/test/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandlerUnitTest.java b/src/test/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandlerUnitTest.java index ad3cdfa0..9b25fed4 100644 --- a/src/test/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandlerUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandlerUnitTest.java @@ -187,7 +187,7 @@ class RestResponseEntityExceptionHandlerUnitTest { final var givenWebRequest = mock(WebRequest.class); // when - final var errorResponse = exceptionHandler.handleIbanAndBicExceptions(givenException, givenWebRequest); + final var errorResponse = exceptionHandler.handleValidationExceptions(givenException, givenWebRequest); // then assertThat(errorResponse.getBody().getStatusCode()).isEqualTo(400); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityUnitTest.java similarity index 95% rename from src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityTest.java rename to src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityUnitTest.java index 4275c56c..154e2b89 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/debitor/HsBookingDebitorEntityUnitTest.java @@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; -class HsBookingDebitorEntityTest { +class HsBookingDebitorEntityUnitTest { @Test void toStringContainsDebitorNumberAndDefaultPrefix() { 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 a0054b4f..2804a758 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 @@ -77,14 +77,14 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup [ { "type": "MANAGED_WEBSPACE", - "caption": "some ManagedWebspace", + "caption": "separate ManagedWebspace", "validFrom": "2022-10-01", "validTo": null, "resources": { - "SDD": 512, - "Multi": 4, - "Daemons": 2, - "Traffic": 12 + "SSD": 100, + "Multi": 1, + "Daemons": 0, + "Traffic": 50 } }, { @@ -94,9 +94,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "validTo": null, "resources": { "RAM": 8, - "SDD": 512, + "SSD": 500, "CPUs": 2, - "Traffic": 42 + "Traffic": 500 } }, { @@ -105,10 +105,11 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "validFrom": "2024-04-01", "validTo": null, "resources": { - "HDD": 10240, - "SDD": 10240, + "HDD": 10000, + "RAM": 32, + "SSD": 4000, "CPUs": 10, - "Traffic": 42 + "Traffic": 2000 } } ] @@ -174,7 +175,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void globalAdmin_canGetArbitraryBookingItem() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingItemUuid = bookingItemRepo.findByCaption("some ManagedWebspace").stream() + final var givenBookingItemUuid = bookingItemRepo.findByCaption("separate ManagedWebspace").stream() .filter(bi -> belongsToDebitorWithDefaultPrefix(bi, "fir")) .map(HsBookingItemEntity::getUuid) .findAny().orElseThrow(); @@ -191,14 +192,14 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .body("", lenientlyEquals(""" { "type": "MANAGED_WEBSPACE", - "caption": "some ManagedWebspace", + "caption": "separate ManagedWebspace", "validFrom": "2022-10-01", "validTo": null, "resources": { - "SDD": 512, - "Multi": 4, - "Daemons": 2, - "Traffic": 12 + "SSD": 100, + "Multi": 1, + "Daemons": 0, + "Traffic": 50 } } """)); // @formatter:on @@ -227,14 +228,16 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup void projectAdmin_canGetRelatedBookingItem() { context.define("superuser-alex@hostsharing.net"); final var givenBookingItemUuid = bookingItemRepo.findByCaption("separate ManagedServer").stream() - .filter(bi -> belongsToDebitorWithDefaultPrefix(bi, "thi")) + .filter(bi -> belongsToDebitorWithDefaultPrefix(bi, "sec")) .map(HsBookingItemEntity::getUuid) .findAny().orElseThrow(); + generateRbacDiagramForObjectPermission(givenBookingItemUuid, "SELECT", "select"); + RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "hs_booking_project#D-1000313-D-1000313defaultproject:ADMIN") + .header("assumed-roles", "hs_booking_project#D-1000212-D-1000212defaultproject:ADMIN") .port(port) .when() .get("http://localhost/api/hs/booking/items/" + givenBookingItemUuid) @@ -249,9 +252,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup "validTo": null, "resources": { "RAM": 8, - "SDD": 512, + "SSD": 500, "CPUs": 2, - "Traffic": 42 + "Traffic": 500 } } """)); // @formatter:on @@ -261,7 +264,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup return ofNullable(bi) .map(HsBookingItemEntity::getProject) .map(HsBookingProjectEntity::getDebitor) - .map(bd -> bd.getDefaultPrefix().equals(defaultPrefix)) + .filter(bd -> bd.getDefaultPrefix().equals(defaultPrefix)) .isPresent(); } } 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 7e312fbc..ca179fc3 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 @@ -44,11 +44,11 @@ class HsBookingItemEntityPatcherUnitTest extends PatchUnitTestBase< private static final Map PATCH_RESOURCES = patchMap( entry("CPU", 2), entry("HDD", null), - entry("SDD", 256) + entry("SSD", 256) ); private static final Map PATCHED_RESOURCES = patchMap( entry("CPU", 2), - entry("SDD", 256), + entry("SSD", 256), entry("MEM", 64) ); 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 0d1e22ac..028971ee 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 @@ -6,7 +6,7 @@ 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; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Nested; @@ -30,7 +30,7 @@ import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGE import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE; 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.mapper.Array.fromFormatted; import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; @@ -174,9 +174,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then allTheseBookingItemsAreReturned( result, - "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, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SDD: 512, Traffic: 42 })", - "HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10240, SDD: 10240, Traffic: 42 })"); + "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 })", + "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 })", + "HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 })"); } @Test @@ -194,9 +194,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then: exactlyTheseBookingItemsAreReturned( result, - "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 })"); + "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 })", + "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 })", + "HsBookingItemEntity(D-1000111:D-1000111 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 })"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorUnitTest.java new file mode 100644 index 00000000..e784edec --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorUnitTest.java @@ -0,0 +1,55 @@ +package net.hostsharing.hsadminng.hs.booking.item.validators; + +import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity; +import org.junit.jupiter.api.Test; + +import jakarta.validation.ValidationException; + +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.PRIVATE_CLOUD; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +class HsBookingItemEntityValidatorUnitTest { + final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder() + .debitorNumber(12345) + .build(); + final HsBookingProjectEntity project = HsBookingProjectEntity.builder() + .debitor(debitor) + .caption("test project") + .build(); + + @Test + void validThrowsException() { + // given + final var cloudServerBookingItemEntity = HsBookingItemEntity.builder() + .type(CLOUD_SERVER) + .project(project) + .caption("Test-Server") + .build(); + + // when + final var result = catchThrowable( ()-> HsBookingItemEntityValidatorRegistry.validated(cloudServerBookingItemEntity)); + + // then + assertThat(result).isInstanceOf(ValidationException.class) + .hasMessageContaining( + "'D-12345:test project:Test-Server.resources.CPUs' is required but missing", + "'D-12345:test project:Test-Server.resources.RAM' is required but missing", + "'D-12345:test project:Test-Server.resources.SSD' is required but missing", + "'D-12345:test project:Test-Server.resources.Traffic' is required but missing"); + } + + @Test + void listsTypes() { + // when + final var result = HsBookingItemEntityValidatorRegistry.types(); + + // then + assertThat(result).containsExactlyInAnyOrder(PRIVATE_CLOUD, CLOUD_SERVER, MANAGED_SERVER, MANAGED_WEBSPACE); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorsUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorsUnitTest.java deleted file mode 100644 index 741d7c1e..00000000 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorsUnitTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package net.hostsharing.hsadminng.hs.booking.item.validators; - -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; -import org.junit.jupiter.api.Test; - -import jakarta.validation.ValidationException; - -import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER; -import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE; -import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.valid; -import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; - -class HsBookingItemEntityValidatorsUnitTest { - - @Test - void validThrowsException() { - // given - final var cloudServerBookingItemEntity = HsBookingItemEntity.builder() - .type(CLOUD_SERVER) - .build(); - - // when - final var result = catchThrowable( ()-> valid(cloudServerBookingItemEntity) ); - - // then - assertThat(result).isInstanceOf(ValidationException.class) - .hasMessageContaining( - "'resources.CPUs' is required but missing", - "'resources.RAM' is required but missing", - "'resources.SSD' is required but missing", - "'resources.Traffic' is required but missing"); - } - - @Test - void listsTypes() { - // when - final var result = HsBookingItemEntityValidators.types(); - - // then - assertThat(result).containsExactlyInAnyOrder(CLOUD_SERVER, MANAGED_SERVER, MANAGED_WEBSPACE); - } -} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java index e15b95d7..787b4c08 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java @@ -1,23 +1,37 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; +import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity; import org.junit.jupiter.api.Test; import java.util.Map; +import static java.util.List.of; import static java.util.Map.entry; +import static java.util.Map.ofEntries; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER; -import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.forType; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.PRIVATE_CLOUD; import static org.assertj.core.api.Assertions.assertThat; class HsCloudServerBookingItemValidatorUnitTest { + final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder() + .debitorNumber(12345) + .build(); + final HsBookingProjectEntity project = HsBookingProjectEntity.builder() + .debitor(debitor) + .caption("Test-Project") + .build(); + @Test void validatesProperties() { // given - final var validator = HsBookingItemEntityValidators.forType(CLOUD_SERVER); final var cloudServerBookingItemEntity = HsBookingItemEntity.builder() .type(CLOUD_SERVER) + .project(project) + .caption("Test-Server") .resources(Map.ofEntries( entry("CPUs", 2), entry("RAM", 25), @@ -28,24 +42,77 @@ class HsCloudServerBookingItemValidatorUnitTest { .build(); // when - final var result = validator.validate(cloudServerBookingItemEntity); + final var result = HsBookingItemEntityValidatorRegistry.doValidate(cloudServerBookingItemEntity); // then - assertThat(result).containsExactly("'resources.SLA-EMail' is not expected but is set to 'true'"); + assertThat(result).containsExactly("'D-12345:Test-Project:Test-Server.resources.SLA-EMail' is not expected but is set to 'true'"); } @Test void containsAllValidations() { // when - final var validator = forType(CLOUD_SERVER); + final var validator = HsBookingItemEntityValidatorRegistry.forType(CLOUD_SERVER); // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( - "{type=integer, propertyName=CPUs, required=true, unit=null, min=1, max=32, step=null}", - "{type=integer, propertyName=RAM, required=true, unit=GB, min=1, max=128, step=null}", - "{type=integer, propertyName=SSD, required=true, unit=GB, min=25, max=1000, step=25}", - "{type=integer, propertyName=HDD, required=false, unit=GB, min=0, max=4000, step=250}", - "{type=integer, propertyName=Traffic, required=true, unit=GB, min=250, max=10000, step=250}", - "{type=enumeration, propertyName=SLA-Infrastructure, required=false, values=[BASIC, EXT8H, EXT4H, EXT2H]}"); + "{type=integer, propertyName=CPUs, min=1, max=32, required=true, isTotalsValidator=false}", + "{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true, isTotalsValidator=false}", + "{type=integer, propertyName=SSD, unit=GB, min=25, max=1000, step=25, required=true, isTotalsValidator=false}", + "{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, required=false, defaultValue=0, isTotalsValidator=false}", + "{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true, isTotalsValidator=false}", + "{type=enumeration, propertyName=SLA-Infrastructure, values=[BASIC, EXT8H, EXT4H, EXT2H], required=false, isTotalsValidator=false}"); + } + + @Test + void validatesExceedingPropertyTotals() { + // given + final var subCloudServerBookingItemEntity = HsBookingItemEntity.builder() + .type(CLOUD_SERVER) + .caption("Test Cloud-Server") + .resources(ofEntries( + entry("CPUs", 2), + entry("RAM", 10), + entry("SSD", 50), + entry("Traffic", 2500) + )) + .build(); + final HsBookingItemEntity subManagedServerBookingItemEntity = HsBookingItemEntity.builder() + .type(MANAGED_SERVER) + .caption("Test Managed-Server") + .resources(ofEntries( + entry("CPUs", 3), + entry("RAM", 20), + entry("SSD", 100), + entry("Traffic", 3000) + )) + .build(); + final var privateCloudBookingItemEntity = HsBookingItemEntity.builder() + .type(PRIVATE_CLOUD) + .project(project) + .caption("Test Cloud") + .resources(ofEntries( + entry("CPUs", 4), + entry("RAM", 20), + entry("SSD", 100), + entry("Traffic", 5000) + )) + .subBookingItems(of( + subManagedServerBookingItemEntity, + subCloudServerBookingItemEntity + )) + .build(); + subManagedServerBookingItemEntity.setParentItem(privateCloudBookingItemEntity); + subCloudServerBookingItemEntity.setParentItem(privateCloudBookingItemEntity); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(subCloudServerBookingItemEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'D-12345:Test-Project:Test Cloud.resources.CPUs' maximum total is 4, but actual total CPUs 5", + "'D-12345:Test-Project:Test Cloud.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB", + "'D-12345:Test-Project:Test Cloud.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB", + "'D-12345:Test-Project:Test Cloud.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB" + ); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java index 5f2bdfc3..1fe54a82 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java @@ -1,56 +1,228 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; +import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import org.junit.jupiter.api.Test; +import java.util.Collection; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import static java.util.Arrays.stream; +import static java.util.List.of; import static java.util.Map.entry; +import static java.util.Map.ofEntries; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER; -import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.forType; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.PRIVATE_CLOUD; import static org.assertj.core.api.Assertions.assertThat; class HsManagedServerBookingItemValidatorUnitTest { + final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder() + .debitorNumber(12345) + .build(); + final HsBookingProjectEntity project = HsBookingProjectEntity.builder() + .debitor(debitor) + .caption("Test-Project") + .build(); + @Test void validatesProperties() { // given - final var validator = HsBookingItemEntityValidators.forType(MANAGED_SERVER); final var mangedServerBookingItemEntity = HsBookingItemEntity.builder() .type(MANAGED_SERVER) + .project(project) .resources(Map.ofEntries( entry("CPUs", 2), entry("RAM", 25), entry("SSD", 25), entry("Traffic", 250), + entry("SLA-Platform", "BASIC"), entry("SLA-EMail", true) )) .build(); // when - final var result = validator.validate(mangedServerBookingItemEntity); + final var result = HsBookingItemEntityValidatorRegistry.doValidate(mangedServerBookingItemEntity); // then - assertThat(result).containsExactly("'resources.SLA-EMail' is expected to be false because resources.SLA-Platform=BASIC but is true"); + assertThat(result).containsExactly("'D-12345:Test-Project:null.resources.SLA-EMail' is expected to be false because SLA-Platform=BASIC but is true"); } @Test void containsAllValidations() { // when - final var validator = forType(MANAGED_SERVER); + final var validator = HsBookingItemEntityValidatorRegistry.forType(MANAGED_SERVER); // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( - "{type=integer, propertyName=CPUs, required=true, unit=null, min=1, max=32, step=null}", - "{type=integer, propertyName=RAM, required=true, unit=GB, min=1, max=128, step=null}", - "{type=integer, propertyName=SSD, required=true, unit=GB, min=25, max=1000, step=25}", - "{type=integer, propertyName=HDD, required=false, unit=GB, min=0, max=4000, step=250}", - "{type=integer, propertyName=Traffic, required=true, unit=GB, min=250, max=10000, step=250}", - "{type=enumeration, propertyName=SLA-Platform, required=false, values=[BASIC, EXT8H, EXT4H, EXT2H]}", - "{type=boolean, propertyName=SLA-EMail, required=false, falseIf={SLA-Platform=BASIC}}", - "{type=boolean, propertyName=SLA-Maria, required=false, falseIf={SLA-Platform=BASIC}}", - "{type=boolean, propertyName=SLA-PgSQL, required=false, falseIf={SLA-Platform=BASIC}}", - "{type=boolean, propertyName=SLA-Office, required=false, falseIf={SLA-Platform=BASIC}}", - "{type=boolean, propertyName=SLA-Web, required=false, falseIf={SLA-Platform=BASIC}}"); + "{type=integer, propertyName=CPUs, min=1, max=32, required=true, isTotalsValidator=false}", + "{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true, isTotalsValidator=false}", + "{type=integer, propertyName=SSD, unit=GB, min=25, max=1000, step=25, required=true, isTotalsValidator=true, thresholdPercentage=200}", + "{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, required=false, defaultValue=0, isTotalsValidator=true, thresholdPercentage=200}", + "{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true, isTotalsValidator=true, thresholdPercentage=200}", + "{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT8H, EXT4H, EXT2H], required=false, defaultValue=BASIC, isTotalsValidator=false}", + "{type=boolean, propertyName=SLA-EMail, required=false, defaultValue=false, isTotalsValidator=false}", + "{type=boolean, propertyName=SLA-Maria, required=false, isTotalsValidator=false}", + "{type=boolean, propertyName=SLA-PgSQL, required=false, isTotalsValidator=false}", + "{type=boolean, propertyName=SLA-Office, required=false, isTotalsValidator=false}", + "{type=boolean, propertyName=SLA-Web, required=false, isTotalsValidator=false}"); } + + @Test + void validatesExceedingPropertyTotals() { + // given + final var subCloudServerBookingItemEntity = HsBookingItemEntity.builder() + .type(CLOUD_SERVER) + .resources(ofEntries( + entry("CPUs", 2), + entry("RAM", 10), + entry("SSD", 50), + entry("Traffic", 2500) + )) + .build(); + final HsBookingItemEntity subManagedServerBookingItemEntity = HsBookingItemEntity.builder() + .type(MANAGED_SERVER) + .resources(ofEntries( + entry("CPUs", 3), + entry("RAM", 20), + entry("SSD", 100), + entry("Traffic", 3000) + )) + .build(); + final var privateCloudBookingItemEntity = HsBookingItemEntity.builder() + .type(PRIVATE_CLOUD) + .project(project) + .resources(ofEntries( + entry("CPUs", 4), + entry("RAM", 20), + entry("SSD", 100), + entry("Traffic", 5000) + )) + .subBookingItems(of( + subManagedServerBookingItemEntity, + subCloudServerBookingItemEntity + )) + .build(); + + subManagedServerBookingItemEntity.setParentItem(privateCloudBookingItemEntity); + subCloudServerBookingItemEntity.setParentItem(privateCloudBookingItemEntity); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(subManagedServerBookingItemEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs 5", + "'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB", + "'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB", + "'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB" + ); + } + + @Test + void validatesExceedingTotals() { + // given + final var managedWebspaceBookingItem = HsBookingItemEntity.builder() + .type(MANAGED_WEBSPACE) + .project(project) + .caption("test Managed-Webspace") + .resources(ofEntries( + entry("SSD", 100), + entry("Traffic", 1000), + entry("Multi", 1) + )) + .subHostingAssets(of( + HsHostingAssetEntity.builder() + .type(HsHostingAssetType.MANAGED_WEBSPACE) + .identifier("abc00") + .subHostingAssets(concat( + generate(26, HsHostingAssetType.UNIX_USER, "xyz00-%c%c"), + generateDbUsersWithDatabases(3, HsHostingAssetType.PGSQL_USER, + "xyz00_%c%c", + 1, HsHostingAssetType.PGSQL_DATABASE + ), + generateDbUsersWithDatabases(3, HsHostingAssetType.MARIADB_USER, + "xyz00_%c%c", + 2, HsHostingAssetType.MARIADB_DATABASE + ), + generateDomainEmailSetupsWithEMailAddresses(26, HsHostingAssetType.DOMAIN_EMAIL_SETUP, + "%c%c.example.com", + 10, HsHostingAssetType.EMAIL_ADDRESS + ) + )) + .build() + )) + .build(); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(managedWebspaceBookingItem); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'D-12345:Test-Project:test Managed-Webspace.resources.Multi=1 allows at maximum 25 unix users, but 26 found", + "'D-12345:Test-Project:test Managed-Webspace.resources.Multi=1 allows at maximum 5 database users, but 6 found", + "'D-12345:Test-Project:test Managed-Webspace.resources.Multi=1 allows at maximum 5 databases, but 9 found", + "'D-12345:Test-Project:test Managed-Webspace.resources.Multi=1 allows at maximum 250 databases, but 260 found" + ); + } + + @SafeVarargs + private List concat(final List... hostingAssets) { + return stream(hostingAssets) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + + private List generate(final int count, final HsHostingAssetType hostingAssetType, + final String identifierPattern) { + return IntStream.range(0, count) + .mapToObj(number -> HsHostingAssetEntity.builder() + .type(hostingAssetType) + .identifier(identifierPattern.formatted((number/'a')+'a', (number%'a')+'a')) + .build()) + .toList(); + } + + private List generateDbUsersWithDatabases( + final int userCount, + final HsHostingAssetType directAssetType, + final String directAssetIdentifierFormat, + final int dbCount, + final HsHostingAssetType subAssetType) { + return IntStream.range(0, userCount) + .mapToObj(n -> HsHostingAssetEntity.builder() + .type(directAssetType) + .identifier(directAssetIdentifierFormat.formatted((n/'a')+'a', (n%'a')+'a')) + .subHostingAssets( + generate(dbCount, subAssetType, "%c%c.example.com".formatted((n/'a')+'a', (n%'a')+'a')) + ) + .build()) + .toList(); + } + + private List generateDomainEmailSetupsWithEMailAddresses( + final int domainCount, + final HsHostingAssetType directAssetType, + final String directAssetIdentifierFormat, + final int emailAddressCount, + final HsHostingAssetType subAssetType) { + return IntStream.range(0, domainCount) + .mapToObj(n -> HsHostingAssetEntity.builder() + .type(directAssetType) + .identifier(directAssetIdentifierFormat.formatted((n/'a')+'a', (n%'a')+'a')) + .subHostingAssets( + generate(emailAddressCount, subAssetType, "xyz00_%c%c%%c%%c".formatted((n/'a')+'a', (n%'a')+'a')) + ) + .build()) + .toList(); + } + } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java index 8a278850..dd9081ee 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java @@ -1,54 +1,66 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; +import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity; import org.junit.jupiter.api.Test; import java.util.Map; import static java.util.Map.entry; import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE; -import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.forType; import static org.assertj.core.api.Assertions.assertThat; class HsManagedWebspaceBookingItemValidatorUnitTest { + final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder() + .debitorNumber(12345) + .build(); + final HsBookingProjectEntity project = HsBookingProjectEntity.builder() + .debitor(debitor) + .caption("Test-Project") + .build(); + @Test void validatesProperties() { // given final var mangedServerBookingItemEntity = HsBookingItemEntity.builder() .type(MANAGED_WEBSPACE) + .project(project) + .caption("Test Managed-Webspace") .resources(Map.ofEntries( entry("CPUs", 2), entry("RAM", 25), - entry("SSD", 25), entry("Traffic", 250), entry("SLA-EMail", true) )) .build(); - final var validator = forType(mangedServerBookingItemEntity.getType()); // when - final var result = validator.validate(mangedServerBookingItemEntity); + final var result = HsBookingItemEntityValidatorRegistry.doValidate(mangedServerBookingItemEntity); // then assertThat(result).containsExactlyInAnyOrder( - "'resources.CPUs' is not expected but is set to '2'", - "'resources.SLA-EMail' is not expected but is set to 'true'", - "'resources.RAM' is not expected but is set to '25'"); + "'D-12345:Test-Project:Test Managed-Webspace.resources.CPUs' is not expected but is set to '2'", + "'D-12345:Test-Project:Test Managed-Webspace.resources.RAM' is not expected but is set to '25'", + "'D-12345:Test-Project:Test Managed-Webspace.resources.SSD' is required but missing", + "'D-12345:Test-Project:Test Managed-Webspace.resources.SLA-EMail' is not expected but is set to 'true'" + ); } @Test void containsAllValidations() { // when - final var validator = forType(MANAGED_WEBSPACE); + final var validator = HsBookingItemEntityValidatorRegistry.forType(MANAGED_WEBSPACE); // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( - "{type=integer, propertyName=SSD, required=true, unit=GB, min=1, max=100, step=1}", - "{type=integer, propertyName=HDD, required=false, unit=GB, min=0, max=250, step=10}", - "{type=integer, propertyName=Traffic, required=true, unit=GB, min=10, max=1000, step=10}", - "{type=enumeration, propertyName=SLA-Platform, required=false, values=[BASIC, EXT24H]}", - "{type=integer, propertyName=Daemons, required=false, unit=null, min=0, max=10, step=null}", - "{type=boolean, propertyName=Online Office Server, required=false, falseIf=null}"); + "{type=integer, propertyName=SSD, unit=GB, min=1, max=100, step=1, required=true, isTotalsValidator=false}", + "{type=integer, propertyName=HDD, unit=GB, min=0, max=250, step=10, required=false, isTotalsValidator=false}", + "{type=integer, propertyName=Traffic, unit=GB, min=10, max=1000, step=10, required=true, isTotalsValidator=false}", + "{type=integer, propertyName=Multi, min=1, max=100, step=1, required=false, defaultValue=1, isTotalsValidator=false}", + "{type=integer, propertyName=Daemons, min=0, max=10, required=false, defaultValue=0, isTotalsValidator=false}", + "{type=boolean, propertyName=Online Office Server, required=false, isTotalsValidator=false}", + "{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT24H], required=false, defaultValue=BASIC, isTotalsValidator=false}"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidatorUnitTest.java new file mode 100644 index 00000000..5079f340 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidatorUnitTest.java @@ -0,0 +1,112 @@ +package net.hostsharing.hsadminng.hs.booking.item.validators; + +import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity; +import org.junit.jupiter.api.Test; + +import static java.util.List.of; +import static java.util.Map.entry; +import static java.util.Map.ofEntries; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER; +import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.PRIVATE_CLOUD; +import static org.assertj.core.api.Assertions.assertThat; + +class HsPrivateCloudBookingItemValidatorUnitTest { + + final HsBookingDebitorEntity debitor = HsBookingDebitorEntity.builder() + .debitorNumber(12345) + .build(); + final HsBookingProjectEntity project = HsBookingProjectEntity.builder() + .debitor(debitor) + .caption("Test-Project") + .build(); + + @Test + void validatesPropertyTotals() { + // given + final var privateCloudBookingItemEntity = HsBookingItemEntity.builder() + .type(PRIVATE_CLOUD) + .resources(ofEntries( + entry("CPUs", 4), + entry("RAM", 20), + entry("SSD", 100), + entry("Traffic", 5000) + )) + .subBookingItems(of( + HsBookingItemEntity.builder() + .type(MANAGED_SERVER) + .resources(ofEntries( + entry("CPUs", 2), + entry("RAM", 10), + entry("SSD", 50), + entry("Traffic", 2500) + )) + .build(), + HsBookingItemEntity.builder() + .type(CLOUD_SERVER) + .resources(ofEntries( + entry("CPUs", 2), + entry("RAM", 10), + entry("SSD", 50), + entry("Traffic", 2500) + )) + .build() + )) + .build(); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(privateCloudBookingItemEntity); + + // then + assertThat(result).isEmpty(); + } + + @Test + void validatesExceedingPropertyTotals() { + // given + final var privateCloudBookingItemEntity = HsBookingItemEntity.builder() + .project(project) + .type(PRIVATE_CLOUD) + .resources(ofEntries( + entry("CPUs", 4), + entry("RAM", 20), + entry("SSD", 100), + entry("Traffic", 5000) + )) + .subBookingItems(of( + HsBookingItemEntity.builder() + .type(MANAGED_SERVER) + .resources(ofEntries( + entry("CPUs", 3), + entry("RAM", 20), + entry("SSD", 100), + entry("Traffic", 3000) + )) + .build(), + HsBookingItemEntity.builder() + .type(CLOUD_SERVER) + .resources(ofEntries( + entry("CPUs", 2), + entry("RAM", 10), + entry("SSD", 50), + entry("Traffic", 2500) + )) + .build() + )) + .build(); + + // when + final var result = HsBookingItemEntityValidatorRegistry.doValidate(privateCloudBookingItemEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs 5", + "'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB", + "'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB", + "'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB" + ); + } + +} 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 index 70676f84..e73bf942 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java @@ -4,7 +4,7 @@ import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorRepository; 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.mapper.Array; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Nested; @@ -23,7 +23,7 @@ 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.mapper.Array.fromFormatted; import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; 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 5204a1ec..e9f8180d 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,7 @@ 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.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; @@ -20,8 +21,9 @@ import java.util.Map; import java.util.UUID; import static java.util.Map.entry; -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.hs.hosting.asset.HsHostingAssetType.UNIX_USER; import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.matchesRegex; @@ -77,25 +79,19 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "type": "MANAGED_WEBSPACE", "identifier": "sec01", "caption": "some Webspace", - "config": { - "extra": 42 - } + "config": {} }, { "type": "MANAGED_WEBSPACE", "identifier": "fir01", "caption": "some Webspace", - "config": { - "extra": 42 - } + "config": {} }, { "type": "MANAGED_WEBSPACE", "identifier": "thi01", "caption": "some Webspace", - "config": { - "extra": 42 - } + "config": {} } ] """)); @@ -110,41 +106,47 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup RestAssured // @formatter:off .given() - .header("current-user", "superuser-alex@hostsharing.net") - .port(port) + .header("current-user", "superuser-alex@hostsharing.net") + .port(port) .when() - .get("http://localhost/api/hs/hosting/assets?type=" + MANAGED_SERVER) + . get("http://localhost/api/hs/hosting/assets?type=" + MANAGED_SERVER) .then().log().all().assertThat() - .statusCode(200) - .contentType("application/json") - .body("", lenientlyEquals(""" - [ - { - "type": "MANAGED_SERVER", - "identifier": "vm1011", - "caption": "some ManagedServer", - "config": { - "extra": 42 + .statusCode(200) + .contentType("application/json") + .body("", lenientlyEquals(""" + [ + { + "type": "MANAGED_SERVER", + "identifier": "vm1011", + "caption": "some ManagedServer", + "config": { + "monit_max_cpu_usage": 90, + "monit_max_ram_usage": 80, + "monit_max_ssd_usage": 70 + } + }, + { + "type": "MANAGED_SERVER", + "identifier": "vm1012", + "caption": "some ManagedServer", + "config": { + "monit_max_cpu_usage": 90, + "monit_max_ram_usage": 80, + "monit_max_ssd_usage": 70 + } + }, + { + "type": "MANAGED_SERVER", + "identifier": "vm1013", + "caption": "some ManagedServer", + "config": { + "monit_max_cpu_usage": 90, + "monit_max_ram_usage": 80, + "monit_max_ssd_usage": 70 + } } - }, - { - "type": "MANAGED_SERVER", - "identifier": "vm1012", - "caption": "some ManagedServer", - "config": { - "extra": 42 - } - }, - { - "type": "MANAGED_SERVER", - "identifier": "vm1013", - "caption": "some ManagedServer", - "config": { - "extra": 42 - } - } - ] - """)); + ] + """)); // @formatter:on } } @@ -156,7 +158,14 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup void globalAdmin_canAddBookedAsset() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingItem = givenBookingItem("D-1000111 default project", "some PrivateCloud"); + final var givenBookingItem = newBookingItem("D-1000111 default project", + HsBookingItemType.MANAGED_WEBSPACE, "separate ManagedWebspace BI", + Map.ofEntries( + entry("SSD", 50), + entry("Traffic", 50) + ) + ); + final var givenParentAsset = givenParentAsset(MANAGED_SERVER, "vm1011"); final var location = RestAssured // @formatter:off .given() @@ -165,12 +174,13 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .body(""" { "bookingItemUuid": "%s", - "type": "MANAGED_SERVER", - "identifier": "vm1400", - "caption": "some new ManagedServer", - "config": { "monit_max_ssd_usage": 80, "monit_max_cpu_usage": 90, "monit_max_ram_usage": 70 } + "type": "MANAGED_WEBSPACE", + "identifier": "fir10", + "parentAssetUuid": "%s", + "caption": "some separate ManagedWebspace HA", + "config": {} } - """.formatted(givenBookingItem.getUuid())) + """.formatted(givenBookingItem.getUuid(), givenParentAsset.getUuid())) .port(port) .when() .post("http://localhost/api/hs/hosting/assets") @@ -179,19 +189,20 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .contentType(ContentType.JSON) .body("", lenientlyEquals(""" { - "type": "MANAGED_SERVER", - "identifier": "vm1400", - "caption": "some new ManagedServer", - "config": { "monit_max_ssd_usage": 80, "monit_max_cpu_usage": 90, "monit_max_ram_usage": 70 } + "type": "MANAGED_WEBSPACE", + "identifier": "fir10", + "caption": "some separate ManagedWebspace HA", + "config": {} } """)) .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*")) .extract().header("Location"); // @formatter:on // finally, the new asset can be accessed under the generated UUID - final var newUserUuid = UUID.fromString( + final var newWebspace = UUID.fromString( location.substring(location.lastIndexOf('/') + 1)); - assertThat(newUserUuid).isNotNull(); + assertThat(newWebspace).isNotNull(); + toCleanup(HsHostingAssetEntity.class, newWebspace); } @Test @@ -240,7 +251,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Test - void additionalValidationsArePerformend_whenAddingAsset() { + void propertyValidationsArePerformend_whenAddingAsset() { context.define("superuser-alex@hostsharing.net"); final var givenBookingItem = givenBookingItem("D-1000111 default project", "some PrivateCloud"); @@ -267,9 +278,66 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .body("", lenientlyEquals(""" { "statusPhrase": "Bad Request", - "message": "['config.extra' is not expected but is set to '42', 'config.monit_max_ssd_usage' is expected to be >= 10 but is 0, 'config.monit_max_cpu_usage' is expected to be <= 100 but is 101, 'config.monit_max_ram_usage' is required but missing]" + "message": "[ + <<<'MANAGED_SERVER:vm1400.config.extra' is not expected but is set to '42', + <<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be >= 10 but is 0, + <<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be <= 100 but is 101, + <<<'MANAGED_SERVER:vm1400.config.monit_max_ram_usage' is required but missing + <<<]" } - """)); // @formatter:on + """.replaceAll(" +<<<", ""))); // @formatter:on + } + + + @Test + void totalsLimitValidationsArePerformend_whenAddingAsset() { + + context.define("superuser-alex@hostsharing.net"); + final var givenHostingAsset = givenHostingAsset(MANAGED_WEBSPACE, "fir01"); + assertThat(givenHostingAsset.getBookingItem().getResources().get("Multi")) + .as("precondition failed") + .isEqualTo(1); + + jpaAttempt.transacted(() -> { + context.define("superuser-alex@hostsharing.net"); + for (int n = 0; n < 25; ++n ) { + toCleanup(assetRepo.save( + HsHostingAssetEntity.builder() + .type(UNIX_USER) + .parentAsset(givenHostingAsset) + .identifier("fir01-%2d".formatted(n)) + .caption("Test UnixUser fir01-%2d".formatted(n)) + .build())); + } + }).assertSuccessful(); + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "parentAssetUuid": "%s", + "type": "UNIX_USER", + "identifier": "fir01-extra", + "caption": "some extra UnixUser", + "config": { } + } + """.formatted(givenHostingAsset.getUuid())) + .port(port) + .when() + .post("http://localhost/api/hs/hosting/assets") + .then().log().all().assertThat() + .statusCode(400) + .contentType(ContentType.JSON) + .body("", lenientlyEquals(""" + { + "statusPhrase": "Bad Request", + "message": "[ + <<<'D-1000111:D-1000111 default project:separate ManagedWebspace.resources.Multi=1 allows at maximum 25 unix users, but 26 found + <<<]" + } + """.replaceAll(" +<<<", ""))); // @formatter:on } } @@ -295,9 +363,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .body("", lenientlyEquals(""" { "caption": "some ManagedServer", - "config": { - "extra": 42 - } + "config": {} } """)); // @formatter:on } @@ -340,9 +406,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup { "identifier": "vm1013", "caption": "some ManagedServer", - "config": { - "extra": 42 - } + "config": {} } """)); // @formatter:on } @@ -443,6 +507,29 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } } + HsHostingAssetEntity givenHostingAsset(final HsHostingAssetType type, final String identifier) { + return assetRepo.findByIdentifier(identifier).stream() + .filter(ha -> ha.getType()==type) + .findAny().orElseThrow(); + } + + HsBookingItemEntity newBookingItem( + final String projectCaption, + final HsBookingItemType type, final String bookingItemCaption, final Map resources) { + return jpaAttempt.transacted(() -> { + context.define("superuser-alex@hostsharing.net"); + final var project = projectRepo.findByCaption(projectCaption).stream() + .findAny().orElseThrow(); + final var bookingItem = HsBookingItemEntity.builder() + .project(project) + .type(type) + .caption(bookingItemCaption) + .resources(resources) + .build(); + return toCleanup(bookingItemRepo.save(bookingItem)); + }).assertSuccessful().returnedValue(); + } + HsBookingItemEntity givenBookingItem(final String projectCaption, final String bookingItemCaption) { return bookingItemRepo.findByCaption(bookingItemCaption).stream() .filter(bi -> bi.getRelatedProject().getCaption().contains(projectCaption)) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java index d726c9b4..2530f5fa 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java @@ -40,11 +40,11 @@ class HsHostingAssetEntityPatcherUnitTest extends PatchUnitTestBase< private static final Map PATCH_CONFIG = patchMap( entry("CPU", 2), entry("HDD", null), - entry("SDD", 256) + entry("SSD", 256) ); private static final Map PATCHED_CONFIG = patchMap( entry("CPU", 2), - entry("SDD", 256), + entry("SSD", 256), entry("MEM", 64) ); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java index 55c2e29e..e8195eeb 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java @@ -33,7 +33,8 @@ class HsHostingAssetPropsControllerAcceptanceTest { [ "MANAGED_SERVER", "MANAGED_WEBSPACE", - "CLOUD_SERVER" + "CLOUD_SERVER", + "UNIX_USER" ] """)); // @formatter:on @@ -55,56 +56,54 @@ class HsHostingAssetPropsControllerAcceptanceTest { { "type": "integer", "propertyName": "monit_min_free_ssd", - "required": false, - "unit": null, "min": 1, "max": 1000, - "step": null + "required": false, + "isTotalsValidator": false }, { "type": "integer", "propertyName": "monit_min_free_hdd", - "required": false, - "unit": null, "min": 1, "max": 4000, - "step": null + "required": false, + "isTotalsValidator": false }, { "type": "integer", "propertyName": "monit_max_ssd_usage", - "required": true, "unit": "%", "min": 10, "max": 100, - "step": null + "required": true, + "isTotalsValidator": false }, { "type": "integer", "propertyName": "monit_max_hdd_usage", - "required": false, "unit": "%", "min": 10, "max": 100, - "step": null + "required": false, + "isTotalsValidator": false }, { "type": "integer", "propertyName": "monit_max_cpu_usage", - "required": true, "unit": "%", "min": 10, "max": 100, - "step": null + "required": true, + "isTotalsValidator": false }, { "type": "integer", "propertyName": "monit_max_ram_usage", - "required": true, "unit": "%", "min": 10, "max": 100, - "step": null + "required": true, + "isTotalsValidator": false } ] """)); 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 f781046a..83560cc9 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,10 +3,11 @@ 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.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository; 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.mapper.Array; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Nested; @@ -30,7 +31,7 @@ import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANA import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; 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.mapper.Array.fromFormatted; import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; @@ -70,12 +71,13 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu // given context("superuser-alex@hostsharing.net"); final var count = assetRepo.count(); - final var givenManagedServer = givenManagedServer("D-1000111 default project", MANAGED_SERVER); + final var givenManagedServer = givenHostingAsset("D-1000111 default project", MANAGED_SERVER); + final var newWebspaceBookingItem = newBookingItem(givenManagedServer.getBookingItem(), HsBookingItemType.MANAGED_WEBSPACE, "fir01"); // when final var result = attempt(em, () -> { final var newAsset = HsHostingAssetEntity.builder() - .bookingItem(givenManagedServer.getBookingItem()) + .bookingItem(newWebspaceBookingItem) .parentAsset(givenManagedServer) .caption("some new managed webspace") .type(MANAGED_WEBSPACE) @@ -95,18 +97,19 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu public void createsAndGrantsRoles() { // given context("superuser-alex@hostsharing.net"); + final var givenManagedServer = givenHostingAsset("D-1000111 default project", MANAGED_SERVER); + final var newWebspaceBookingItem = newBookingItem(givenManagedServer.getBookingItem(), HsBookingItemType.MANAGED_WEBSPACE, "fir01"); + em.flush(); final var initialRoleNames = distinctRoleNamesOf(rawRoleRepo.findAll()); - final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()).stream() - .map(s -> s.replace("hs_office_", "")) - .toList(); - final var givenBookingItem = givenBookingItem("D-1000111 default project", "some PrivateCloud"); + final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()); // when final var result = attempt(em, () -> { final var newAsset = HsHostingAssetEntity.builder() - .bookingItem(givenBookingItem) - .type(HsHostingAssetType.MANAGED_SERVER) - .identifier("vm9000") + .bookingItem(newWebspaceBookingItem) + .parentAsset(givenManagedServer) + .type(HsHostingAssetType.MANAGED_WEBSPACE) + .identifier("fir00") .caption("some new managed webspace") .build(); return toCleanup(assetRepo.save(newAsset)); @@ -117,29 +120,33 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu final var all = rawRoleRepo.findAll(); assertThat(distinctRoleNamesOf(all)).containsExactlyInAnyOrder(Array.from( initialRoleNames, - "hs_hosting_asset#vm9000:OWNER", - "hs_hosting_asset#vm9000:ADMIN", - "hs_hosting_asset#vm9000:AGENT", - "hs_hosting_asset#vm9000:TENANT")); + "hs_hosting_asset#fir00:ADMIN", + "hs_hosting_asset#fir00:AGENT", + "hs_hosting_asset#fir00:OWNER", + "hs_hosting_asset#fir00:TENANT")); assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())) - .map(s -> s.replace("hs_office_", "")) .containsExactlyInAnyOrder(fromFormatted( initialGrantNames, // owner - "{ 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 }", + "{ grant role:hs_hosting_asset#fir00:OWNER to role:hs_booking_item#fir01:ADMIN by system and assume }", + "{ grant role:hs_hosting_asset#fir00:OWNER to role:hs_hosting_asset#vm1011:ADMIN by system and assume }", + "{ grant perm:hs_hosting_asset#fir00:DELETE to role:hs_hosting_asset#fir00:OWNER by system and assume }", // admin - "{ 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 }", + "{ grant role:hs_hosting_asset#fir00:ADMIN to role:hs_hosting_asset#fir00:OWNER by system and assume }", + "{ grant role:hs_hosting_asset#fir00:ADMIN to role:hs_booking_item#fir01:AGENT by system and assume }", + "{ grant perm:hs_hosting_asset#fir00:INSERT>hs_hosting_asset to role:hs_hosting_asset#fir00:ADMIN by system and assume }", + "{ grant perm:hs_hosting_asset#fir00:UPDATE to role:hs_hosting_asset#fir00:ADMIN by system and assume }", + + // agent + "{ grant role:hs_hosting_asset#fir00:ADMIN to role:hs_hosting_asset#vm1011:AGENT by system and assume }", + "{ grant role:hs_hosting_asset#fir00:AGENT to role:hs_hosting_asset#fir00:ADMIN by system and assume }", // tenant - "{ grant role:hs_booking_item#somePrivateCloud:TENANT to role:hs_hosting_asset#vm9000:TENANT by system and assume }", + "{ grant role:hs_booking_item#fir01:TENANT to role:hs_hosting_asset#fir00:TENANT by system and assume }", + "{ grant role:hs_hosting_asset#fir00:TENANT to role:hs_hosting_asset#fir00:AGENT by system and assume }", + "{ grant role:hs_hosting_asset#vm1011:TENANT to role:hs_hosting_asset#fir00:TENANT by system and assume }", null)); } @@ -164,9 +171,9 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu // then allTheseServersAreReturned( result, - "HsHostingAssetEntity(MANAGED_WEBSPACE, sec01, some Webspace, MANAGED_SERVER:vm1012, D-1000212:D-1000212 default project:separate ManagedServer, { extra: 42 })", - "HsHostingAssetEntity(MANAGED_WEBSPACE, thi01, some Webspace, MANAGED_SERVER:vm1013, D-1000313:D-1000313 default project:separate ManagedServer, { extra: 42 })", - "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedServer, { extra: 42 })"); + "HsHostingAssetEntity(MANAGED_WEBSPACE, sec01, some Webspace, MANAGED_SERVER:vm1012, D-1000212:D-1000212 default project:separate ManagedWebspace)", + "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedWebspace)", + "HsHostingAssetEntity(MANAGED_WEBSPACE, thi01, some Webspace, MANAGED_SERVER:vm1013, D-1000313:D-1000313 default project:separate ManagedWebspace)"); } @Test @@ -182,9 +189,8 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu // then: exactlyTheseAssetsAreReturned( result, - "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedServer, { extra: 42 })", - "HsHostingAssetEntity(MANAGED_SERVER, vm1011, some ManagedServer, D-1000111:D-1000111 default project:some PrivateCloud, { extra: 42 })", - "HsHostingAssetEntity(CLOUD_SERVER, vm2011, another CloudServer, D-1000111:D-1000111 default project:some PrivateCloud, { extra: 42 })"); + "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedWebspace)", + "HsHostingAssetEntity(MANAGED_SERVER, vm1011, some ManagedServer, D-1000111:D-1000111 default project:separate ManagedServer, { monit_max_cpu_usage: 90, monit_max_ram_usage: 80, monit_max_ssd_usage: 70 })"); } @Test @@ -200,7 +206,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu // then allTheseServersAreReturned( result, - "HsHostingAssetEntity(MANAGED_WEBSPACE, thi01, some Webspace, MANAGED_SERVER:vm1013, D-1000313:D-1000313 default project:separate ManagedServer, { extra: 42 })"); + "HsHostingAssetEntity(MANAGED_WEBSPACE, sec01, some Webspace, MANAGED_SERVER:vm1012, D-1000212:D-1000212 default project:separate ManagedWebspace)"); } } @@ -351,7 +357,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu private HsHostingAssetEntity givenSomeTemporaryAsset(final String projectCaption, final String identifier) { return jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net"); - final var givenBookingItem = givenBookingItem("D-1000111 default project", "some PrivateCloud"); + final var givenBookingItem = givenBookingItem("D-1000111 default project", "test CloudServer"); final var newAsset = HsHostingAssetEntity.builder() .bookingItem(givenBookingItem) .type(CLOUD_SERVER) @@ -367,20 +373,30 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu } HsBookingItemEntity givenBookingItem(final String projectCaption, final String bookingItemCaption) { - final var givenProject = projectRepo.findByCaption(projectCaption).stream() - .findAny().orElseThrow(); - return bookingItemRepo.findAllByProjectUuid(givenProject.getUuid()).stream() - .filter(i -> i.getCaption().equals(bookingItemCaption)) + return bookingItemRepo.findByCaption(bookingItemCaption).stream() + .filter(i -> i.getRelatedProject().getCaption().equals(projectCaption)) .findAny().orElseThrow(); } - HsHostingAssetEntity givenManagedServer(final String projectCaption, final HsHostingAssetType type) { + HsHostingAssetEntity givenHostingAsset(final String projectCaption, final HsHostingAssetType type) { final var givenProject = projectRepo.findByCaption(projectCaption).stream() .findAny().orElseThrow(); return assetRepo.findAllByCriteria(givenProject.getUuid(), null, type).stream() .findAny().orElseThrow(); } + HsBookingItemEntity newBookingItem( + final HsBookingItemEntity parentBookingItem, + final HsBookingItemType type, + final String caption) { + final var newBookingItem = HsBookingItemEntity.builder() + .parentItem(parentBookingItem) + .type(type) + .caption(caption) + .build(); + return toCleanup(bookingItemRepo.save(newBookingItem)); + } + void exactlyTheseAssetsAreReturned( final List actualResult, final String... serverNames) { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java index de679c40..ee6644e0 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java @@ -7,7 +7,6 @@ import java.util.Map; import static java.util.Map.entry; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER; -import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators.forType; import static org.assertj.core.api.Assertions.assertThat; class HsCloudServerHostingAssetValidatorUnitTest { @@ -17,24 +16,25 @@ class HsCloudServerHostingAssetValidatorUnitTest { // given final var cloudServerHostingAssetEntity = HsHostingAssetEntity.builder() .type(CLOUD_SERVER) + .identifier("vm1234") .config(Map.ofEntries( entry("RAM", 2000) )) .build(); - final var validator = forType(cloudServerHostingAssetEntity.getType()); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(cloudServerHostingAssetEntity.getType()); // when final var result = validator.validate(cloudServerHostingAssetEntity); // then - assertThat(result).containsExactly("'config.RAM' is not expected but is set to '2000'"); + assertThat(result).containsExactly("'CLOUD_SERVER:vm1234.config.RAM' is not expected but is set to '2000'"); } @Test void containsAllValidations() { // when - final var validator = forType(CLOUD_SERVER); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(CLOUD_SERVER); // then assertThat(validator.properties()).map(Map::toString).isEmpty(); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorsUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java similarity index 60% rename from src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorsUnitTest.java rename to src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java index 0e07e30c..b92e5dc9 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorsUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java @@ -6,27 +6,27 @@ import org.junit.jupiter.api.Test; import jakarta.validation.ValidationException; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; -import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators.valid; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; -class HsHostingAssetEntityValidatorsUnitTest { +class HsHostingAssetEntityValidatorUnitTest { @Test void validThrowsException() { // given final var managedServerHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_SERVER) + .identifier("vm1234") .build(); // when - final var result = catchThrowable( ()-> valid(managedServerHostingAssetEntity) ); + final var result = catchThrowable( ()-> HsHostingAssetEntityValidatorRegistry.validated(managedServerHostingAssetEntity)); // then assertThat(result).isInstanceOf(ValidationException.class) .hasMessageContaining( - "'config.monit_max_ssd_usage' is required but missing", - "'config.monit_max_cpu_usage' is required but missing", - "'config.monit_max_ram_usage' is required but missing"); + "'MANAGED_SERVER:vm1234.config.monit_max_ssd_usage' is required but missing", + "'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is required but missing", + "'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is required but missing"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java index cb9e066b..b8e75436 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java @@ -7,7 +7,6 @@ import java.util.Map; 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.validators.HsHostingAssetEntityValidators.forType; import static org.assertj.core.api.Assertions.assertThat; class HsManagedServerHostingAssetValidatorUnitTest { @@ -17,22 +16,23 @@ class HsManagedServerHostingAssetValidatorUnitTest { // given final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_SERVER) + .identifier("vm1234") .config(Map.ofEntries( entry("monit_max_hdd_usage", "90"), entry("monit_max_cpu_usage", 2), entry("monit_max_ram_usage", 101) )) .build(); - final var validator = forType(mangedWebspaceHostingAssetEntity.getType()); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedWebspaceHostingAssetEntity.getType()); // when final var result = validator.validate(mangedWebspaceHostingAssetEntity); // then assertThat(result).containsExactlyInAnyOrder( - "'config.monit_max_ssd_usage' is required but missing", - "'config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'", - "'config.monit_max_cpu_usage' is expected to be >= 10 but is 2", - "'config.monit_max_ram_usage' is expected to be <= 100 but is 101"); + "'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be >= 10 but is 2", + "'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be <= 100 but is 101", + "'MANAGED_SERVER:vm1234.config.monit_max_ssd_usage' is required but missing", + "'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'"); } } 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 83634501..d2e74894 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 @@ -1,13 +1,14 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import org.junit.jupiter.api.Test; import java.util.Map; 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; @@ -16,21 +17,32 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { final HsBookingItemEntity managedServerBookingItem = HsBookingItemEntity.builder() .project(TEST_PROJECT) + .type(HsBookingItemType.MANAGED_SERVER) + .caption("Test Managed-Server") + .resources(Map.ofEntries( + entry("CPUs", 2), + entry("RAM", 25), + entry("SSD", 25), + entry("Traffic", 250), + entry("SLA-Platform", "EXT4H"), + entry("SLA-EMail", true) + )) .build(); final HsHostingAssetEntity mangedServerAssetEntity = HsHostingAssetEntity.builder() - .type(MANAGED_SERVER) + .type(HsHostingAssetType.MANAGED_SERVER) .bookingItem(managedServerBookingItem) + .identifier("vm1234") .config(Map.ofEntries( - entry("HDD", 0), - entry("SSD", 1), - entry("Traffic", 10) + entry("monit_max_ssd_usage", 70), + entry("monit_max_cpu_usage", 80), + entry("monit_max_ram_usage", 90) )) .build(); @Test void validatesIdentifier() { // given - final var validator = HsHostingAssetEntityValidators.forType(MANAGED_WEBSPACE); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE); final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_WEBSPACE) .parentAsset(mangedServerAssetEntity) @@ -47,7 +59,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { @Test void validatesUnknownProperties() { // given - final var validator = HsHostingAssetEntityValidators.forType(MANAGED_WEBSPACE); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE); final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_WEBSPACE) .parentAsset(mangedServerAssetEntity) @@ -61,13 +73,13 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { final var result = validator.validate(mangedWebspaceHostingAssetEntity); // then - assertThat(result).containsExactly("'config.unknown' is not expected but is set to 'some value'"); + assertThat(result).containsExactly("'MANAGED_WEBSPACE:abc00.config.unknown' is not expected but is set to 'some value'"); } @Test void validatesValidEntity() { // given - final var validator = HsHostingAssetEntityValidators.forType(MANAGED_WEBSPACE); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE); final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_WEBSPACE) .parentAsset(mangedServerAssetEntity) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java index c46210c4..d7d07f69 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java @@ -4,7 +4,7 @@ import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; 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.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.Nested; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java index cca5c48c..4e591973 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java @@ -4,7 +4,7 @@ import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; 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.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.Nested; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java index ff6c9315..0c9215f9 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java @@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipReposito import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; 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.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java index 65f85b58..e6163cd4 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java @@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipReposito import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; 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.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; 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 b2e54d06..856356cf 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 @@ -11,7 +11,7 @@ import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantRepository; import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService; import net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleRepository; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.hibernate.Hibernate; import org.junit.jupiter.api.Disabled; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java index 1cba78da..701e6651 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java @@ -7,7 +7,7 @@ import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; 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.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java index 39faf7eb..5daf0f8f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java @@ -30,7 +30,7 @@ import java.util.Objects; import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf; import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacObjectEntity.objectDisplaysOf; import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf; -import static net.hostsharing.hsadminng.rbac.test.Array.from; +import static net.hostsharing.hsadminng.mapper.Array.from; import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java index 7ce2fdf1..efd7064f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java @@ -4,7 +4,7 @@ import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; 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.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.Nested; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java index fe9e2ef1..0792d656 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationRepositoryIntegrationTest.java @@ -6,7 +6,7 @@ import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; 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.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java index 5544a3e3..ad7ee76e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java @@ -7,7 +7,7 @@ import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; 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.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -26,7 +26,7 @@ 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.mapper.Array.fromFormatted; import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java b/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java index 11cda37f..22e1df04 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/context/ContextIntegrationTests.java @@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.rbac.context; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.mapper.Mapper; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java index 536d748c..e7a28261 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.rbac.rbacrole; import net.hostsharing.hsadminng.context.Context; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java index f1e6fef5..be6377a0 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java @@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.rbac.rbacuser; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.rbac.context.ContextBasedTest; -import net.hostsharing.hsadminng.rbac.test.Array; +import net.hostsharing.hsadminng.mapper.Array; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; From cbadc6e2c7b83ab27d13c9c79477f280edbd64e1 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 17 Jun 2024 16:46:26 +0200 Subject: [PATCH 04/18] mitigate-hosting-asset-fetching-performance-problems (#60) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/60 Reviewed-by: Marc Sandlus --- .../hs/booking/item/HsBookingItemEntity.java | 5 +- .../hosting/asset/HsHostingAssetEntity.java | 11 ++-- ...HsBookingItemControllerAcceptanceTest.java | 50 +++++++++++-------- ...sBookingItemRepositoryIntegrationTest.java | 4 +- ...sHostingAssetControllerAcceptanceTest.java | 4 +- ...HostingAssetRepositoryIntegrationTest.java | 11 ++-- .../test/ContextBasedTestWithCleanup.java | 41 ++++++++------- 7 files changed, 74 insertions(+), 52 deletions(-) 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 b820c243..6daa8ba9 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 @@ -24,6 +24,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; @@ -82,11 +83,11 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { @Version private int version; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "projectuuid") private HsBookingProjectEntity project; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parentitemuuid") private HsBookingItemEntity parentItem; 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 3f8202ef..164e42d0 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 @@ -21,6 +21,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; @@ -77,15 +78,15 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject { @Version private int version; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "bookingitemuuid") private HsBookingItemEntity bookingItem; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parentassetuuid") private HsHostingAssetEntity parentAsset; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "assignedtoassetuuid") private HsHostingAssetEntity assignedToAsset; @@ -93,12 +94,12 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject { @Enumerated(EnumType.STRING) private HsHostingAssetType type; - @OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true) + @OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true, fetch = FetchType.LAZY) @JoinColumn(name="parentassetuuid", referencedColumnName="uuid") private List subHostingAssets; @Column(name = "identifier") - private String identifier; // vm1234, xyz00, example.org, xyz00_abc + private String identifier; // e.g. vm1234, xyz00, example.org, xyz00_abc @Column(name = "caption") private String caption; 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 2804a758..bf8b4ba9 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,13 +4,17 @@ 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.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; +import org.junit.jupiter.api.ClassOrderer; +import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestClassOrder; +import org.junit.jupiter.api.TestMethodOrder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; @@ -33,6 +37,7 @@ import static org.hamcrest.Matchers.matchesRegex; classes = { HsadminNgApplication.class, JpaAttempt.class } ) @Transactional +@TestClassOrder(ClassOrderer.OrderAnnotation.class) // fail early on fetching problems class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup { @LocalServerPort @@ -51,6 +56,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup JpaAttempt jpaAttempt; @Nested + @Order(2) class ListBookingItems { @Test @@ -119,6 +125,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Nested + @Order(3) class AddBookingItem { @Test @@ -170,13 +177,16 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Nested + @Order(1) + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class GetBookingItem { @Test + @Order(1) void globalAdmin_canGetArbitraryBookingItem() { context.define("superuser-alex@hostsharing.net"); final var givenBookingItemUuid = bookingItemRepo.findByCaption("separate ManagedWebspace").stream() - .filter(bi -> belongsToDebitorWithDefaultPrefix(bi, "fir")) + .filter(bi -> belongsToProject(bi, "D-1000111 default project")) .map(HsBookingItemEntity::getUuid) .findAny().orElseThrow(); @@ -206,10 +216,11 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Test + @Order(2) void normalUser_canNotGetUnrelatedBookingItem() { context.define("superuser-alex@hostsharing.net"); final var givenBookingItemUuid = bookingItemRepo.findByCaption("separate ManagedServer").stream() - .filter(bi -> belongsToDebitorWithDefaultPrefix(bi, "sec")) + .filter(bi -> belongsToProject(bi, "D-1000212 default project")) .map(HsBookingItemEntity::getUuid) .findAny().orElseThrow(); @@ -224,23 +235,21 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Test - // TODO.impl: For unknown reason, this test fails in about 50%, not finding the uuid (404), maybe no SELECT permission? + @Order(3) + // TODO.impl: For unknown reason this test fails in about 50%, not finding the uuid (404). void projectAdmin_canGetRelatedBookingItem() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingItemUuid = bookingItemRepo.findByCaption("separate ManagedServer").stream() - .filter(bi -> belongsToDebitorWithDefaultPrefix(bi, "sec")) - .map(HsBookingItemEntity::getUuid) + final var givenBookingItem = bookingItemRepo.findByCaption("separate ManagedServer").stream() + .filter(bi -> belongsToProject(bi, "D-1000313 default project")) .findAny().orElseThrow(); - generateRbacDiagramForObjectPermission(givenBookingItemUuid, "SELECT", "select"); - RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "hs_booking_project#D-1000212-D-1000212defaultproject:ADMIN") + .header("assumed-roles", "hs_booking_project#D-1000313-D-1000313defaultproject:ADMIN") .port(port) .when() - .get("http://localhost/api/hs/booking/items/" + givenBookingItemUuid) + .get("http://localhost/api/hs/booking/items/" + givenBookingItem.getUuid()) .then().log().all().assertThat() .statusCode(200) .contentType("application/json") @@ -260,22 +269,22 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup """)); // @formatter:on } - private static boolean belongsToDebitorWithDefaultPrefix(final HsBookingItemEntity bi, final String defaultPrefix) { + private static boolean belongsToProject(final HsBookingItemEntity bi, final String projectCaption) { return ofNullable(bi) .map(HsBookingItemEntity::getProject) - .map(HsBookingProjectEntity::getDebitor) - .filter(bd -> bd.getDefaultPrefix().equals(defaultPrefix)) + .filter(bp -> bp.getCaption().equals(projectCaption)) .isPresent(); } } @Nested + @Order(4) class PatchBookingItem { @Test void globalAdmin_canPatchAllUpdatablePropertiesOfBookingItem() { - final var givenBookingItem = givenSomeBookingItem(1000111, MANAGED_WEBSPACE, + final var givenBookingItem = givenSomeNewBookingItem("D-1000111 default project", MANAGED_WEBSPACE, resource("HDD", 100), resource("SSD", 50), resource("Traffic", 250)); RestAssured // @formatter:off @@ -324,12 +333,13 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Nested + @Order(5) class DeleteBookingItem { @Test void globalAdmin_canDeleteArbitraryBookingItem() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingItem = givenSomeBookingItem(1000111, MANAGED_WEBSPACE, + final var givenBookingItem = givenSomeNewBookingItem("D-1000111 default project", MANAGED_WEBSPACE, resource("HDD", 100), resource("SSD", 50), resource("Traffic", 250)); RestAssured // @formatter:off @@ -348,7 +358,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void normalUser_canNotDeleteUnrelatedBookingItem() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingItem = givenSomeBookingItem(1000111, MANAGED_WEBSPACE, + final var givenBookingItem = givenSomeNewBookingItem("D-1000111 default project", MANAGED_WEBSPACE, resource("HDD", 100), resource("SSD", 50), resource("Traffic", 250)); RestAssured // @formatter:off @@ -366,13 +376,11 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup } @SafeVarargs - private HsBookingItemEntity givenSomeBookingItem(final int debitorNumber, + private HsBookingItemEntity givenSomeNewBookingItem(final String projectCaption, final HsBookingItemType hsBookingItemType, final Map.Entry... resources) { return jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net"); - final var givenProject = debitorRepo.findDebitorByDebitorNumber(debitorNumber).stream() - .map(d -> projectRepo.findAllByDebitorUuid(d.getUuid())) - .flatMap(java.util.List::stream) + final var givenProject = projectRepo.findByCaption(projectCaption).stream() .findAny().orElseThrow(); final var newBookingItem = HsBookingItemEntity.builder() .uuid(UUID.randomUUID()) 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 028971ee..fcc290ff 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 @@ -231,7 +231,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup private void assertThatBookingItemActuallyInDatabase(final HsBookingItemEntity saved) { final var found = bookingItemRepo.findByUuid(saved.getUuid()); assertThat(found).isNotEmpty().get().isNotSameAs(saved) - .extracting(Object::toString).isEqualTo(saved.toString()); + .extracting(HsBookingItemEntity::getResources) + .extracting(Object::toString) + .isEqualTo(saved.getResources().toString()); } } 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 e9f8180d..2ea554c6 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 @@ -458,8 +458,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup context.define("superuser-alex@hostsharing.net"); assertThat(assetRepo.findByUuid(givenAsset.getUuid())).isPresent().get() .matches(asset -> { - assertThat(asset.toString()).isEqualTo( - "HsHostingAssetEntity(MANAGED_SERVER, vm2001, some test-asset, D-1000111:D-1000111 default project:some ManagedServer, { monit_max_cpu_usage: 90, monit_max_ram_usage: 70, monit_max_ssd_usage: 85, monit_min_free_ssd: 5 })"); + assertThat(asset.getConfig().toString()).isEqualTo( + "{ monit_max_cpu_usage: 90, monit_max_ram_usage: 70, monit_max_ssd_usage: 85, monit_min_free_ssd: 5 }"); return true; }); } 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 83560cc9..480f9416 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 @@ -24,6 +24,8 @@ import jakarta.servlet.http.HttpServletRequest; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import static java.util.Map.entry; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER; @@ -152,8 +154,11 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu } private void assertThatAssetIsPersisted(final HsHostingAssetEntity saved) { - final var found = assetRepo.findByUuid(saved.getUuid()); - assertThat(found).isNotEmpty().map(HsHostingAssetEntity::toString).get().isEqualTo(saved.toString()); + attempt(em, () -> { + context("superuser-alex@hostsharing.net"); + final var found = assetRepo.findByUuid(saved.getUuid()); + assertThat(found).isNotEmpty().map(HsHostingAssetEntity::toString).get().isEqualTo(saved.toString()); + }); } } @@ -240,7 +245,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu private void assertThatAssetActuallyInDatabase(final HsHostingAssetEntity saved) { final var found = assetRepo.findByUuid(saved.getUuid()); assertThat(found).isNotEmpty().get().isNotSameAs(saved) - .extracting(Object::toString).isEqualTo(saved.toString()); + .extracting(HsHostingAssetEntity::getVersion).isEqualTo(saved.getVersion()); } } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java b/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java index 154dbb11..6c9ac849 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java @@ -63,7 +63,6 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { return merged; } - // remove HsOfficeCoopAssetsTransactionRawEntity, which is not needed anymore after this change public UUID toCleanup(final Class entityClass, final UUID uuidToCleanup) { out.println("toCleanup(" + entityClass.getSimpleName() + ", " + uuidToCleanup + ")"); entitiesToCleanup.put(uuidToCleanup, entityClass); @@ -176,7 +175,8 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { } private void cleanupTemporaryTestData() { - jpaAttempt.transacted(() -> { + // For better performance in a single transaction ... + final var exception = jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net", null); entitiesToCleanup.reversed().forEach((uuid, entityClass) -> { final var rvTableName = entityClass.getAnnotation(Table.class).name(); @@ -188,7 +188,12 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { .setParameter("uuid", uuid).executeUpdate(); out.println("DELETING temporary " + entityClass.getSimpleName() + "#" + uuid + " deleted " + deletedRows + " rows"); }); - }).assertSuccessful(); + }).caughtException(); + + // ... and in case of foreign key violations, we rely on the RbacObject cleanup. + if (exception != null) { + System.err.println(exception); + } } private long assertNoNewRbacObjectsRolesAndGrantsLeaked() { @@ -214,24 +219,24 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { } private void deleteLeakedRbacObjects() { - jpaAttempt.transacted(() -> rbacObjectRepo.findAll()).returnedValue().stream() - .filter(o -> o.serialId > latestIntialTestDataSerialId) - .sorted(comparing(o -> o.serialId)) - .forEach(o -> { - final var exception = jpaAttempt.transacted(() -> { - context.define("superuser-alex@hostsharing.net", null); + rbacObjectRepo.findAll().stream() + .filter(o -> o.serialId > latestIntialTestDataSerialId) + .sorted(comparing(o -> o.serialId)) + .forEach(o -> { + final var exception = jpaAttempt.transacted(() -> { + context.define("superuser-alex@hostsharing.net", null); - em.createNativeQuery("DELETE FROM " + o.objectTable + " WHERE uuid=:uuid") - .setParameter("uuid", o.uuid) - .executeUpdate(); + em.createNativeQuery("DELETE FROM " + o.objectTable + " WHERE uuid=:uuid") + .setParameter("uuid", o.uuid) + .executeUpdate(); - out.println("DELETING leaked " + o.objectTable + "#" + o.uuid + " SUCCEEDED"); - }).caughtException(); + out.println("DELETING leaked " + o.objectTable + "#" + o.uuid + " SUCCEEDED"); + }).caughtException(); - if (exception != null) { - out.println("DELETING leaked " + o.objectTable + "#" + o.uuid + " FAILED " + exception); - } - }); + if (exception != null) { + out.println("DELETING leaked " + o.objectTable + "#" + o.uuid + " FAILED " + exception); + } + }); } private void assertEqual(final Set before, final Set after) { From 62867a4caca028726660466216dd448efbd8bc01 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 18 Jun 2024 13:53:11 +0200 Subject: [PATCH 05/18] booking-item-to-related-hosting-asset-just-1-to-1 (#61) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/61 Reviewed-by: Marc Sandlus --- .../hs/booking/item/HsBookingItemEntity.java | 8 +-- ...HsManagedWebspaceBookingItemValidator.java | 49 ++++++++++--------- ...sBookingItemRepositoryIntegrationTest.java | 3 ++ ...gedServerBookingItemValidatorUnitTest.java | 41 ++++++++-------- 4 files changed, 55 insertions(+), 46 deletions(-) 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 6daa8ba9..0856f866 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 @@ -17,6 +17,8 @@ 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 org.hibernate.annotations.NotFound; +import org.hibernate.annotations.NotFoundAction; import org.hibernate.annotations.Type; import jakarta.persistence.CascadeType; @@ -30,6 +32,7 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import jakarta.persistence.Transient; import jakarta.persistence.Version; @@ -113,9 +116,8 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { @JoinColumn(name="parentitemuuid", referencedColumnName="uuid") private List subBookingItems; - @OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true) - @JoinColumn(name="bookingitemuuid", referencedColumnName="uuid") - private List subHostingAssets; + @OneToOne(mappedBy="bookingItem") + private HsHostingAssetEntity relatedHostingAsset; @Transient private PatchableMapWrapper resourcesWrapper; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java index bf637f15..81c74b9f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java @@ -7,6 +7,7 @@ import org.apache.commons.lang3.function.TriFunction; import java.util.List; import static java.util.Collections.emptyList; +import static java.util.Optional.ofNullable; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_EMAIL_SETUP; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.EMAIL_ADDRESS; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_DATABASE; @@ -38,10 +39,11 @@ class HsManagedWebspaceBookingItemValidator extends HsBookingItemEntityValidator private static TriFunction> unixUsers() { return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> { - final var unixUserCount = entity.getSubHostingAssets().stream() - .flatMap(ha -> ha.getSubHostingAssets().stream()) - .filter(ha -> ha.getType() == UNIX_USER) - .count(); + final var unixUserCount = ofNullable(entity.getRelatedHostingAsset()) + .map(ha -> ha.getSubHostingAssets().stream() + .filter(subAsset -> subAsset.getType() == UNIX_USER) + .count()) + .orElse(0L); final long limitingValue = prop.getValue(entity.getResources()); if (unixUserCount > factor*limitingValue) { return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " unix users, but " + unixUserCount + " found"); @@ -52,13 +54,14 @@ class HsManagedWebspaceBookingItemValidator extends HsBookingItemEntityValidator private static TriFunction> databaseUsers() { return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> { - final var unixUserCount = entity.getSubHostingAssets().stream() - .flatMap(ha -> ha.getSubHostingAssets().stream()) - .filter(bi -> bi.getType() == PGSQL_USER || bi.getType() == MARIADB_USER ) - .count(); + final var dbUserCount = ofNullable(entity.getRelatedHostingAsset()) + .map(ha -> ha.getSubHostingAssets().stream() + .filter(bi -> bi.getType() == PGSQL_USER || bi.getType() == MARIADB_USER ) + .count()) + .orElse(0L); final long limitingValue = prop.getValue(entity.getResources()); - if (unixUserCount > factor*limitingValue) { - return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " database users, but " + unixUserCount + " found"); + if (dbUserCount > factor*limitingValue) { + return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " database users, but " + dbUserCount + " found"); } return emptyList(); }; @@ -66,12 +69,13 @@ class HsManagedWebspaceBookingItemValidator extends HsBookingItemEntityValidator private static TriFunction> databases() { return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> { - final var unixUserCount = entity.getSubHostingAssets().stream() - .flatMap(ha -> ha.getSubHostingAssets().stream()) - .filter(bi -> bi.getType()==PGSQL_USER || bi.getType()==MARIADB_USER ) - .flatMap(domainEMailSetup -> domainEMailSetup.getSubHostingAssets().stream() - .filter(ha -> ha.getType()==PGSQL_DATABASE || ha.getType()==MARIADB_DATABASE)) - .count(); + final var unixUserCount = ofNullable(entity.getRelatedHostingAsset()) + .map(ha -> ha.getSubHostingAssets().stream() + .filter(bi -> bi.getType()==PGSQL_USER || bi.getType()==MARIADB_USER ) + .flatMap(domainEMailSetup -> domainEMailSetup.getSubHostingAssets().stream() + .filter(subAsset -> subAsset.getType()==PGSQL_DATABASE || subAsset.getType()==MARIADB_DATABASE)) + .count()) + .orElse(0L); final long limitingValue = prop.getValue(entity.getResources()); if (unixUserCount > factor*limitingValue) { return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " databases, but " + unixUserCount + " found"); @@ -82,12 +86,13 @@ class HsManagedWebspaceBookingItemValidator extends HsBookingItemEntityValidator private static TriFunction> eMailAddresses() { return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> { - final var unixUserCount = entity.getSubHostingAssets().stream() - .flatMap(ha -> ha.getSubHostingAssets().stream()) - .filter(bi -> bi.getType() == DOMAIN_EMAIL_SETUP) - .flatMap(domainEMailSetup -> domainEMailSetup.getSubHostingAssets().stream() - .filter(ha -> ha.getType()==EMAIL_ADDRESS)) - .count(); + final var unixUserCount = ofNullable(entity.getRelatedHostingAsset()) + .map(ha -> ha.getSubHostingAssets().stream() + .filter(bi -> bi.getType() == DOMAIN_EMAIL_SETUP) + .flatMap(domainEMailSetup -> domainEMailSetup.getSubHostingAssets().stream() + .filter(subAsset -> subAsset.getType()==EMAIL_ADDRESS)) + .count()) + .orElse(0L); final long limitingValue = prop.getValue(entity.getResources()); if (unixUserCount > factor*limitingValue) { return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " databases, but " + unixUserCount + " found"); 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 fcc290ff..b474d0c7 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 @@ -177,6 +177,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 })", "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 })", "HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 })"); + assertThat(result.stream().filter(bi -> bi.getRelatedHostingAsset()!=null).findAny()) + .as("at least one relatedProject expected, but none found => fetching relatedProject does not work") + .isNotEmpty(); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java index 1fe54a82..549b5700 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java @@ -139,27 +139,26 @@ class HsManagedServerBookingItemValidatorUnitTest { entry("Traffic", 1000), entry("Multi", 1) )) - .subHostingAssets(of( - HsHostingAssetEntity.builder() - .type(HsHostingAssetType.MANAGED_WEBSPACE) - .identifier("abc00") - .subHostingAssets(concat( - generate(26, HsHostingAssetType.UNIX_USER, "xyz00-%c%c"), - generateDbUsersWithDatabases(3, HsHostingAssetType.PGSQL_USER, - "xyz00_%c%c", - 1, HsHostingAssetType.PGSQL_DATABASE - ), - generateDbUsersWithDatabases(3, HsHostingAssetType.MARIADB_USER, - "xyz00_%c%c", - 2, HsHostingAssetType.MARIADB_DATABASE - ), - generateDomainEmailSetupsWithEMailAddresses(26, HsHostingAssetType.DOMAIN_EMAIL_SETUP, - "%c%c.example.com", - 10, HsHostingAssetType.EMAIL_ADDRESS - ) - )) - .build() - )) + .relatedHostingAsset(HsHostingAssetEntity.builder() + .type(HsHostingAssetType.MANAGED_WEBSPACE) + .identifier("abc00") + .subHostingAssets(concat( + generate(26, HsHostingAssetType.UNIX_USER, "xyz00-%c%c"), + generateDbUsersWithDatabases(3, HsHostingAssetType.PGSQL_USER, + "xyz00_%c%c", + 1, HsHostingAssetType.PGSQL_DATABASE + ), + generateDbUsersWithDatabases(3, HsHostingAssetType.MARIADB_USER, + "xyz00_%c%c", + 2, HsHostingAssetType.MARIADB_DATABASE + ), + generateDomainEmailSetupsWithEMailAddresses(26, HsHostingAssetType.DOMAIN_EMAIL_SETUP, + "%c%c.example.com", + 10, HsHostingAssetType.EMAIL_ADDRESS + ) + )) + .build() + ) .build(); // when From 04d9b433016f2698776fc100d3337356ef0addd7 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 20 Jun 2024 10:44:28 +0200 Subject: [PATCH 06/18] BookingItem validity start date today (#62) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/62 Reviewed-by: Marc Sandlus --- .../booking/item/HsBookingItemController.java | 4 +- .../hs/booking/item/HsBookingItemEntity.java | 4 +- .../hs-booking/hs-booking-item-schemas.yaml | 4 - ...HsBookingItemControllerAcceptanceTest.java | 21 ++- .../item/HsBookingItemControllerRestTest.java | 171 ++++++++++++++++++ .../item/HsBookingItemEntityUnitTest.java | 24 +++ ...HostingAssetRepositoryIntegrationTest.java | 2 - 7 files changed, 211 insertions(+), 19 deletions(-) create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerRestTest.java 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 1343378c..36c16a32 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 @@ -16,6 +16,7 @@ import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBui import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; +import java.time.LocalDate; import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; @@ -130,9 +131,8 @@ public class HsBookingItemController implements HsBookingItemsApi { } }; - @SuppressWarnings("unchecked") final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { - entity.setValidity(toPostgresDateRange(resource.getValidFrom(), resource.getValidTo())); + entity.setValidity(toPostgresDateRange(LocalDate.now(), resource.getValidTo())); entity.putResources(KeyValueMap.from(resource.getResources())); }; } 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 0856f866..90774110 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 @@ -17,8 +17,6 @@ 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 org.hibernate.annotations.NotFound; -import org.hibernate.annotations.NotFoundAction; import org.hibernate.annotations.Type; import jakarta.persistence.CascadeType; @@ -101,7 +99,7 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { @Builder.Default @Type(PostgreSQLRangeType.class) @Column(name = "validity", columnDefinition = "daterange") - private Range validity = Range.emptyRange(LocalDate.class); + private Range validity = Range.closedInfinite(LocalDate.now()); @Column(name = "caption") private String caption; 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 aa7ab925..b18c7356 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 @@ -62,10 +62,6 @@ components: minLength: 3 maxLength: 80 nullable: false - validFrom: - type: string - format: date - nullable: false validTo: type: string format: date 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 bf8b4ba9..5edc23af 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 @@ -144,13 +144,16 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup .contentType(ContentType.JSON) .body(""" { - "projectUuid": "%s", + "projectUuid": "{projectUuid}", "type": "MANAGED_SERVER", "caption": "some new booking", - "resources": { "CPUs": 12, "RAM": 4, "SSD": 100, "Traffic": 250 }, - "validFrom": "2022-10-13" + "validTo": "{validTo}", + "resources": { "CPUs": 12, "RAM": 4, "SSD": 100, "Traffic": 250 } } - """.formatted(givenProject.getUuid())) + """ + .replace("{projectUuid}", givenProject.getUuid().toString()) + .replace("{validTo}", LocalDate.now().plusMonths(1).toString()) + ) .port(port) .when() .post("http://localhost/api/hs/booking/items") @@ -161,11 +164,14 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup { "type": "MANAGED_SERVER", "caption": "some new booking", - "validFrom": "2022-10-13", - "validTo": null, + "validFrom": "{today}", + "validTo": "{todayPlus1Month}", "resources": { "CPUs": 12, "SSD": 100, "Traffic": 250 } } - """)) + """ + .replace("{today}", LocalDate.now().toString()) + .replace("{todayPlus1Month}", LocalDate.now().plusMonths(1).toString())) + ) .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/booking/items/[^/]*")) .extract().header("Location"); // @formatter:on @@ -236,7 +242,6 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test @Order(3) - // TODO.impl: For unknown reason this test fails in about 50%, not finding the uuid (404). void projectAdmin_canGetRelatedBookingItem() { context.define("superuser-alex@hostsharing.net"); final var givenBookingItem = bookingItemRepo.findByCaption("separate ManagedServer").stream() diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerRestTest.java new file mode 100644 index 00000000..0fb0f6f0 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerRestTest.java @@ -0,0 +1,171 @@ +package net.hostsharing.hsadminng.hs.booking.item; + +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity; +import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository; +import net.hostsharing.hsadminng.mapper.Mapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.SynchronizationType; +import java.time.LocalDate; +import java.util.Map; +import java.util.UUID; + +import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; +import static org.hamcrest.Matchers.matchesRegex; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(HsBookingItemController.class) +@Import(Mapper.class) +@RunWith(SpringRunner.class) +class HsBookingItemControllerRestTest { + + @Autowired + MockMvc mockMvc; + + @MockBean + Context contextMock; + + @Mock + EntityManager em; + + @MockBean + EntityManagerFactory emf; + + @MockBean + HsBookingProjectRepository bookingProjectRepo; + + @MockBean + HsBookingItemRepository bookingItemRepo; + + @BeforeEach + void init() { + when(emf.createEntityManager()).thenReturn(em); + when(emf.createEntityManager(any(Map.class))).thenReturn(em); + when(emf.createEntityManager(any(SynchronizationType.class))).thenReturn(em); + when(emf.createEntityManager(any(SynchronizationType.class), any(Map.class))).thenReturn(em); + } + + @Nested + class AddBookingItem { + + @Test + void globalAdmin_canAddValidBookingItem() throws Exception { + + final var givenProjectUuid = UUID.randomUUID(); + + // given + when(em.find(HsBookingProjectEntity.class, givenProjectUuid)).thenAnswer(invocation -> + HsBookingProjectEntity.builder() + .uuid(invocation.getArgument(1)) + .build() + ); + when(bookingItemRepo.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + + // when + mockMvc.perform(MockMvcRequestBuilders + .post("/api/hs/booking/items") + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "projectUuid": "{projectUuid}", + "type": "MANAGED_SERVER", + "caption": "some new booking", + "validTo": "{validTo}", + "garbage": "should not be accepted", + "resources": { "CPUs": 12, "RAM": 4, "SSD": 100, "Traffic": 250 } + } + """ + .replace("{projectUuid}", givenProjectUuid.toString()) + .replace("{validTo}", LocalDate.now().plusMonths(1).toString()) + ) + .accept(MediaType.APPLICATION_JSON)) + + // then + .andExpect(status().isCreated()) + .andExpect(jsonPath("$", lenientlyEquals(""" + { + "type": "MANAGED_SERVER", + "caption": "some new booking", + "validFrom": "{today}", + "validTo": "{todayPlus1Month}", + "resources": { "CPUs": 12, "SSD": 100, "Traffic": 250 } + } + """ + .replace("{today}", LocalDate.now().toString()) + .replace("{todayPlus1Month}", LocalDate.now().plusMonths(1).toString())) + )) + .andExpect(header().string("Location", matchesRegex("http://localhost/api/hs/booking/items/[^/]*"))); + } + + @Test + void globalAdmin_canNotAddInvalidBookingItem() throws Exception { + + final var givenProjectUuid = UUID.randomUUID(); + + // given + when(em.find(HsBookingProjectEntity.class, givenProjectUuid)).thenAnswer(invocation -> + HsBookingProjectEntity.builder() + .uuid(invocation.getArgument(1)) + .build() + ); + when(bookingItemRepo.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + + // when + mockMvc.perform(MockMvcRequestBuilders + .post("/api/hs/booking/items") + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "projectUuid": "{projectUuid}", + "type": "MANAGED_SERVER", + "caption": "some new booking", + "validFrom": "{validFrom}", + "resources": { "CPUs": 12, "RAM": 4, "SSD": 100, "Traffic": 250 } + } + """ + .replace("{projectUuid}", givenProjectUuid.toString()) + .replace("{validFrom}", LocalDate.now().plusMonths(1).toString()) + ) + .accept(MediaType.APPLICATION_JSON)) + + // then + // TODO.test: MockMvc does not seem to validate additionalProperties=false + // .andExpect(status().is4xxClientError()) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$", lenientlyEquals(""" + { + "type": "MANAGED_SERVER", + "caption": "some new booking", + "validFrom": "{today}", + "validTo": null, + "resources": { "CPUs": 12, "SSD": 100, "Traffic": 250 } + } + """ + .replace("{today}", LocalDate.now().toString()) + .replace("{todayPlus1Month}", LocalDate.now().plusMonths(1).toString())) + )) + .andExpect(header().string("Location", matchesRegex("http://localhost/api/hs/booking/items/[^/]*"))); + } + } +} 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 1b95dc8a..903d5385 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 @@ -1,8 +1,12 @@ package net.hostsharing.hsadminng.hs.booking.item; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import java.time.LocalDate; +import java.time.Month; import java.util.Map; import static java.util.Map.entry; @@ -14,6 +18,8 @@ class HsBookingItemEntityUnitTest { public static final LocalDate GIVEN_VALID_FROM = LocalDate.parse("2020-01-01"); public static final LocalDate GIVEN_VALID_TO = LocalDate.parse("2030-12-31"); + private MockedStatic localDateMockedStatic = Mockito.mockStatic(LocalDate.class, Mockito.CALLS_REAL_METHODS); + final HsBookingItemEntity givenBookingItem = HsBookingItemEntity.builder() .project(TEST_PROJECT) .type(HsBookingItemType.CLOUD_SERVER) @@ -25,6 +31,24 @@ class HsBookingItemEntityUnitTest { .validity(toPostgresDateRange(GIVEN_VALID_FROM, GIVEN_VALID_TO)) .build(); + @AfterEach + void tearDown() { + localDateMockedStatic.close(); + } + + @Test + void validityStartsToday() { + // given + final var fakedToday = LocalDate.of(2024, Month.MAY, 1); + localDateMockedStatic.when(LocalDate::now).thenReturn(fakedToday); + + // when + final var newBookingItem = HsBookingItemEntity.builder().build(); + + // then + assertThat(newBookingItem.getValidity().toString()).isEqualTo("Range{lower=2024-05-01, upper=null, mask=82, clazz=class java.time.LocalDate}"); + } + @Test void toStringContainsAllPropertiesAndResourcesSortedByKey() { final var result = givenBookingItem.toString(); 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 480f9416..f4abe06c 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 @@ -24,8 +24,6 @@ import jakarta.servlet.http.HttpServletRequest; import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; import static java.util.Map.entry; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER; From d157730de7949e48a6d34cb4e3d2893de4738c28 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 20 Jun 2024 11:03:59 +0200 Subject: [PATCH 07/18] finalize PrivateCloud, Cloud- and ManagedServer and ManagedWebspace Billingtems and HostingAssets (#63) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/63 Reviewed-by: Marc Sandlus --- .../errors/MultiValidationException.java | 6 +- .../HsBookingItemEntityValidator.java | 14 +- .../HsCloudServerBookingItemValidator.java | 22 ++- .../HsPrivateCloudBookingItemValidator.java | 36 +++- .../asset/HsHostingAssetController.java | 9 + .../hosting/asset/HsHostingAssetEntity.java | 4 +- .../HsHostingAssetEntityValidator.java | 6 +- .../HsManagedServerHostingAssetValidator.java | 44 ++++- .../hs/validation/EnumerationProperty.java | 22 ++- .../hs/validation/HsEntityValidator.java | 13 +- .../hs/validation/ValidatableProperty.java | 29 +++ ...oudServerBookingItemValidatorUnitTest.java | 11 +- ...gedServerBookingItemValidatorUnitTest.java | 8 +- ...vateCloudBookingItemValidatorUnitTest.java | 51 +++-- ...sHostingAssetControllerAcceptanceTest.java | 88 +++++---- ...ingAssetPropsControllerAcceptanceTest.java | 180 ++++++++++++++++-- ...HsHostingAssetEntityValidatorUnitTest.java | 7 +- ...edServerHostingAssetValidatorUnitTest.java | 1 - ...fficeDebitorRepositoryIntegrationTest.java | 2 +- .../test/ContextBasedTestWithCleanup.java | 35 +++- 20 files changed, 458 insertions(+), 130 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java b/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java index 9a6d459d..a6ba69e8 100644 --- a/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java +++ b/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java @@ -8,7 +8,11 @@ import static java.lang.String.join; public class MultiValidationException extends ValidationException { private MultiValidationException(final List violations) { - super("[\n" + join(",\n", violations) + "\n]"); + super( + violations.size() > 1 + ? "[\n" + join(",\n", violations) + "\n]" + : "[" + join(",\n", violations) + "]" + ); } public static void throwInvalid(final List violations) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java index 7d002bac..ee07e981 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; import net.hostsharing.hsadminng.hs.validation.ValidatableProperty; +import org.apache.commons.lang3.BooleanUtils; import java.util.Collection; import java.util.List; @@ -59,19 +60,24 @@ public class HsBookingItemEntityValidator extends HsEntityValidator propDef.getValue(subItem.getResources())) - .map(HsBookingItemEntityValidator::toNonNullInteger) + .map(HsBookingItemEntityValidator::convertBooleanToInteger) + .map(HsBookingItemEntityValidator::toIntegerWithDefault0) .reduce(0, Integer::sum); - final var maxValue = getNonNullIntegerValue(propDef, bookingItem.getResources()); + final var maxValue = getIntegerValueWithDefault0(propDef, bookingItem.getResources()); if (propDef.thresholdPercentage() != null ) { return totalValue > (maxValue * propDef.thresholdPercentage() / 100) - ? "%s' maximum total is %d%s, but actual total %s %d%s, which exceeds threshold of %d%%" + ? "%s' maximum total is %d%s, but actual total %s is %d%s, which exceeds threshold of %d%%" .formatted(propName, maxValue, propUnit, propName, totalValue, propUnit, propDef.thresholdPercentage()) : null; } else { return totalValue > maxValue - ? "%s' maximum total is %d%s, but actual total %s %d%s" + ? "%s' maximum total is %d%s, but actual total %s is %d%s" .formatted(propName, maxValue, propUnit, propName, totalValue, propUnit) : null; } } + + private static Object convertBooleanToInteger(final Object value) { + return value instanceof Boolean ? BooleanUtils.toInteger((Boolean)value) : value; + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidator.java index 07bb80da..d673f01a 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidator.java @@ -1,7 +1,6 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; - - +import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty; import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty; import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; @@ -9,12 +8,21 @@ class HsCloudServerBookingItemValidator extends HsBookingItemEntityValidator { HsCloudServerBookingItemValidator() { super( - integerProperty("CPUs").min(1).max(32).required(), - integerProperty("RAM").unit("GB").min(1).max(128).required(), - integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(), - integerProperty("HDD").unit("GB").min(0).max(4000).step(250).withDefault(0), - integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(), + // @formatter:off + booleanProperty("active") .withDefault(true), + + integerProperty("CPUs") .min( 1) .max( 32) .required(), + integerProperty("RAM").unit("GB") .min( 1) .max( 128) .required(), + integerProperty("SSD").unit("GB") .min( 0) .max( 1000) .step(25).required(), // (1) + integerProperty("HDD").unit("GB") .min( 0) .max( 4000) .step(250).withDefault(0), + integerProperty("Traffic").unit("GB") .min(250) .max(10000) .step(250).required(), + enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional() + // @formatter:on ); + + // (q) We do have pre-existing CloudServers without SSD, just HDD, thus SSD starts with min=0. + // TODO.impl: Validation that SSD+HDD is at minimum 25 GB is missing. + // e.g. validationGroup("SSD", "HDD").min(0); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidator.java index 317f2f0c..236a000a 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidator.java @@ -1,18 +1,40 @@ package net.hostsharing.hsadminng.hs.booking.item.validators; -import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty; import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; class HsPrivateCloudBookingItemValidator extends HsBookingItemEntityValidator { HsPrivateCloudBookingItemValidator() { super( - integerProperty("CPUs").min(4).max(128).required().asTotalLimit(), - integerProperty("RAM").unit("GB").min(4).max(512).required().asTotalLimit(), - integerProperty("SSD").unit("GB").min(100).max(4000).step(25).required().asTotalLimit(), - integerProperty("HDD").unit("GB").min(0).max(16000).step(25).withDefault(0).asTotalLimit(), - integerProperty("Traffic").unit("GB").min(1000).max(40000).step(250).required().asTotalLimit(), - enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").withDefault("BASIC") + // @formatter:off + integerProperty("CPUs") .min( 1).max( 128).required().asTotalLimit(), + integerProperty("RAM").unit("GB") .min( 1).max( 512).required().asTotalLimit(), + integerProperty("SSD").unit("GB") .min( 25).max( 4000).step(25).required().asTotalLimit(), + integerProperty("HDD").unit("GB") .min( 0).max(16000).step(250).withDefault(0).asTotalLimit(), + integerProperty("Traffic").unit("GB") .min(250).max(40000).step(250).required().asTotalLimit(), + +// Alternatively we could specify it similarly to "Multi" option but exclusively counting: +// integerProperty("Resource-Points") .min(4).max(100).required() +// .each("CPUs").countsAs(64) +// .each("RAM").countsAs(64) +// .each("SSD").countsAs(18) +// .each("HDD").countsAs(2) +// .each("Traffic").countsAs(1), + + integerProperty("SLA-Infrastructure EXT8H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Infrastructure", "EXT8H"), + integerProperty("SLA-Infrastructure EXT4H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Infrastructure", "EXT4H"), + integerProperty("SLA-Infrastructure EXT2H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Infrastructure", "EXT2H"), + + integerProperty("SLA-Platform EXT8H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Platform", "EXT8H"), + integerProperty("SLA-Platform EXT4H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Platform", "EXT4H"), + integerProperty("SLA-Platform EXT2H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Platform", "EXT2H"), + + integerProperty("SLA-EMail") .min( 0).max( 20).withDefault(0).asTotalLimit(), + integerProperty("SLA-Maria") .min( 0).max( 20).withDefault(0).asTotalLimit(), + integerProperty("SLA-PgSQL") .min( 0).max( 20).withDefault(0).asTotalLimit(), + integerProperty("SLA-Office") .min( 0).max( 20).withDefault(0).asTotalLimit(), + integerProperty("SLA-Web") .min( 0).max( 20).withDefault(0).asTotalLimit() + // @formatter:on ); } } 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 76003671..d3578833 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 @@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetsApi; import net.hostsharing.hsadminng.context.Context; @@ -34,6 +35,9 @@ public class HsHostingAssetController implements HsHostingAssetsApi { @Autowired private HsHostingAssetRepository assetRepo; + @Autowired + private HsBookingItemRepository bookingItemRepo; + @Override @Transactional(readOnly = true) public ResponseEntity> listAssets( @@ -124,6 +128,11 @@ public class HsHostingAssetController implements HsHostingAssetsApi { final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { entity.putConfig(KeyValueMap.from(resource.getConfig())); + if (resource.getBookingItemUuid() != null) { + entity.setBookingItem(bookingItemRepo.findByUuid(resource.getBookingItemUuid()) + .orElseThrow(() -> new EntityNotFoundException("ERROR: [400] bookingItemUuid %s not found".formatted( + resource.getBookingItemUuid())))); + } if (resource.getParentAssetUuid() != null) { entity.setParentAsset(assetRepo.findByUuid(resource.getParentAssetUuid()) .orElseThrow(() -> new EntityNotFoundException("ERROR: [400] parentAssetUuid %s not found".formatted( 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 164e42d0..fa15537a 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 @@ -27,6 +27,7 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import jakarta.persistence.Transient; import jakarta.persistence.Version; @@ -78,7 +79,7 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject { @Version private int version; - @ManyToOne(fetch = FetchType.LAZY) + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "bookingitemuuid") private HsBookingItemEntity bookingItem; @@ -142,7 +143,6 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject { dependsOnColumn("bookingItemUuid"), directlyFetchedByDependsOnColumn(), NULLABLE) - .toRole("bookingItem", AGENT).grantPermission(INSERT) .importEntityAlias("parentAsset", HsHostingAssetEntity.class, usingDefaultCase(), dependsOnColumn("parentAssetUuid"), diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java index 3a0438ee..c452d378 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java @@ -65,11 +65,11 @@ public class HsHostingAssetEntityValidator extends HsEntityValidator propDef.getValue(subItem.getConfig())) - .map(HsEntityValidator::toNonNullInteger) + .map(HsEntityValidator::toIntegerWithDefault0) .reduce(0, Integer::sum); - final var maxValue = getNonNullIntegerValue(propDef, hostingAsset.getConfig()); + final var maxValue = getIntegerValueWithDefault0(propDef, hostingAsset.getConfig()); return totalValue > maxValue - ? "%s' maximum total is %d%s, but actual total is %s %d%s".formatted( + ? "%s' maximum total is %d%s, but actual total %s is %d%s".formatted( propName, maxValue, propUnit, propName, totalValue, propUnit) : null; } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java index b2107866..00050010 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java @@ -1,18 +1,48 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; +import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty; +import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty; import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; class HsManagedServerHostingAssetValidator extends HsHostingAssetEntityValidator { public HsManagedServerHostingAssetValidator() { super( - integerProperty("monit_min_free_ssd").min(1).max(1000).optional(), - integerProperty("monit_min_free_hdd").min(1).max(4000).optional(), - integerProperty("monit_max_ssd_usage").unit("%").min(10).max(100).required(), - integerProperty("monit_max_hdd_usage").unit("%").min(10).max(100).optional(), - integerProperty("monit_max_cpu_usage").unit("%").min(10).max(100).required(), - integerProperty("monit_max_ram_usage").unit("%").min(10).max(100).required() - // TODO: stringProperty("monit_alarm_email").unit("GB").optional() + // monitoring + integerProperty("monit_max_cpu_usage").unit("%").min(10).max(100).withDefault(92), + integerProperty("monit_max_ram_usage").unit("%").min(10).max(100).withDefault(92), + integerProperty("monit_max_ssd_usage").unit("%").min(10).max(100).withDefault(98), + integerProperty("monit_min_free_ssd").min(1).max(1000).withDefault(5), + integerProperty("monit_max_hdd_usage").unit("%").min(10).max(100).withDefault(95), + integerProperty("monit_min_free_hdd").min(1).max(4000).withDefault(10), + // stringProperty("monit_alarm_email").unit("GB").optional() TODO.impl: via Contact? + + // other settings + // booleanProperty("fastcgi_small").withDefault(false), TODO.spec: clarify Salt-Grains + + // database software + booleanProperty("software-pgsql").withDefault(true), + booleanProperty("software-mariadb").withDefault(true), + + // PHP + enumerationProperty("php-default").valuesFromProperties("software-php-").withDefault("8.2"), + booleanProperty("software-php-5.6").withDefault(false), + booleanProperty("software-php-7.0").withDefault(false), + booleanProperty("software-php-7.1").withDefault(false), + booleanProperty("software-php-7.2").withDefault(false), + booleanProperty("software-php-7.3").withDefault(false), + booleanProperty("software-php-7.4").withDefault(true), + booleanProperty("software-php-8.0").withDefault(false), + booleanProperty("software-php-8.1").withDefault(false), + booleanProperty("software-php-8.2").withDefault(true), + + // other software + booleanProperty("software-postfix-tls-1.0").withDefault(false), + booleanProperty("software-dovecot-tls-1.0").withDefault(false), + booleanProperty("software-clamav").withDefault(true), + booleanProperty("software-collabora").withDefault(false), + booleanProperty("software-libreoffice").withDefault(false), + booleanProperty("software-imagemagick-ghostscript").withDefault(false) ); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java index 23e5ef61..923d7ae1 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java @@ -7,6 +7,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Map; +import static java.util.Arrays.stream; + @Setter public class EnumerationProperty extends ValidatableProperty { @@ -30,9 +32,27 @@ public class EnumerationProperty extends ValidatableProperty { return this; } + public void deferredInit(final ValidatableProperty[] allProperties) { + if (deferredInit != null) { + if (this.values != null) { + throw new IllegalStateException("property " + toString() + " already has values"); + } + this.values = deferredInit.apply(allProperties); + } + } + + public ValidatableProperty valuesFromProperties(final String propertyNamePrefix) { + this.deferredInit = (ValidatableProperty[] allProperties) -> stream(allProperties) + .map(ValidatableProperty::propertyName) + .filter(name -> name.startsWith(propertyNamePrefix)) + .map(name -> name.substring(propertyNamePrefix.length())) + .toArray(String[]::new); + return this; + } + @Override protected void validate(final ArrayList result, final String propValue, final Map props) { - if (Arrays.stream(values).noneMatch(v -> v.equals(propValue))) { + if (stream(values).noneMatch(v -> v.equals(propValue))) { result.add(propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'"); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java index c06ed140..4c20f2a5 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java @@ -16,6 +16,7 @@ public abstract class HsEntityValidator { public HsEntityValidator(final ValidatableProperty... validators) { propertyValidators = validators; + stream(propertyValidators).forEach(p -> p.deferredInit(propertyValidators)); } protected static List enrich(final String prefix, final List messages) { @@ -59,18 +60,24 @@ public abstract class HsEntityValidator { .orElse(emptyList())); } - protected static Integer getNonNullIntegerValue(final ValidatableProperty prop, final Map propValues) { + protected static Integer getIntegerValueWithDefault0(final ValidatableProperty prop, final Map propValues) { final var value = prop.getValue(propValues); if (value instanceof Integer) { return (Integer) value; } + if (value == null) { + return 0; + } throw new IllegalArgumentException(prop.propertyName + " Integer value expected, but got " + value); } - protected static Integer toNonNullInteger(final Object value) { + protected static Integer toIntegerWithDefault0(final Object value) { if (value instanceof Integer) { return (Integer) value; } - throw new IllegalArgumentException("Integer value expected, but got " + value); + if (value == null) { + return 0; + } + throw new IllegalArgumentException("Integer value (or null) expected, but got " + value); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java index 7795d47d..3b0bb099 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java @@ -19,6 +19,7 @@ import java.util.function.Function; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; import static java.util.Collections.emptyList; +import static java.util.Optional.ofNullable; @RequiredArgsConstructor public abstract class ValidatableProperty { @@ -31,6 +32,7 @@ public abstract class ValidatableProperty { private final String[] keyOrder; private Boolean required; private T defaultValue; + protected Function[], T[]> deferredInit; private boolean isTotalsValidator = false; @JsonIgnore private List>> asTotalLimitValidators; // TODO.impl: move to BookingItemIntegerProperty @@ -57,11 +59,38 @@ public abstract class ValidatableProperty { return this; } + public void deferredInit(final ValidatableProperty[] allProperties) { + } + public ValidatableProperty asTotalLimit() { isTotalsValidator = true; return this; } + public ValidatableProperty asTotalLimitFor(final String propertyName, final String propertyValue) { + if (asTotalLimitValidators == null) { + asTotalLimitValidators = new ArrayList<>(); + } + final TriFunction> validator = + (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> { + + final var total = entity.getSubBookingItems().stream() + .map(server -> server.getResources().get(propertyName)) + .filter(propertyValue::equals) + .count(); + + final long limitingValue = ofNullable(prop.getValue(entity.getResources())).orElse(0); + if (total > factor*limitingValue) { + return List.of( + prop.propertyName() + " maximum total is " + (factor*limitingValue) + ", but actual total for " + propertyName + "=" + propertyValue + " is " + total + ); + } + return emptyList(); + }; + asTotalLimitValidators.add((final HsBookingItemEntity entity) -> validator.apply(entity, (IntegerProperty)this, 1)); + return this; + } + public String propertyName() { return propertyName; } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java index 787b4c08..9258a4a1 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java @@ -55,9 +55,10 @@ class HsCloudServerBookingItemValidatorUnitTest { // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( + "{type=boolean, propertyName=active, required=false, defaultValue=true, isTotalsValidator=false}", "{type=integer, propertyName=CPUs, min=1, max=32, required=true, isTotalsValidator=false}", "{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true, isTotalsValidator=false}", - "{type=integer, propertyName=SSD, unit=GB, min=25, max=1000, step=25, required=true, isTotalsValidator=false}", + "{type=integer, propertyName=SSD, unit=GB, min=0, max=1000, step=25, required=true, isTotalsValidator=false}", "{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, required=false, defaultValue=0, isTotalsValidator=false}", "{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true, isTotalsValidator=false}", "{type=enumeration, propertyName=SLA-Infrastructure, values=[BASIC, EXT8H, EXT4H, EXT2H], required=false, isTotalsValidator=false}"); @@ -109,10 +110,10 @@ class HsCloudServerBookingItemValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( - "'D-12345:Test-Project:Test Cloud.resources.CPUs' maximum total is 4, but actual total CPUs 5", - "'D-12345:Test-Project:Test Cloud.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB", - "'D-12345:Test-Project:Test Cloud.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB", - "'D-12345:Test-Project:Test Cloud.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB" + "'D-12345:Test-Project:Test Cloud.resources.CPUs' maximum total is 4, but actual total CPUs is 5", + "'D-12345:Test-Project:Test Cloud.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB", + "'D-12345:Test-Project:Test Cloud.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB", + "'D-12345:Test-Project:Test Cloud.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB" ); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java index 549b5700..1fe754ea 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java @@ -120,10 +120,10 @@ class HsManagedServerBookingItemValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( - "'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs 5", - "'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB", - "'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB", - "'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB" + "'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs is 5", + "'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB", + "'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB", + "'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB" ); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidatorUnitTest.java index 5079f340..2a100d2c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsPrivateCloudBookingItemValidatorUnitTest.java @@ -28,29 +28,38 @@ class HsPrivateCloudBookingItemValidatorUnitTest { // given final var privateCloudBookingItemEntity = HsBookingItemEntity.builder() .type(PRIVATE_CLOUD) + .caption("myPC") .resources(ofEntries( entry("CPUs", 4), entry("RAM", 20), entry("SSD", 100), - entry("Traffic", 5000) + entry("Traffic", 5000), + entry("SLA-Platform EXT4H", 2), + entry("SLA-EMail", 2) )) .subBookingItems(of( HsBookingItemEntity.builder() .type(MANAGED_SERVER) + .caption("myMS-1") .resources(ofEntries( entry("CPUs", 2), entry("RAM", 10), entry("SSD", 50), - entry("Traffic", 2500) + entry("Traffic", 2500), + entry("SLA-Platform", "EXT4H"), + entry("SLA-EMail", true) )) .build(), HsBookingItemEntity.builder() .type(CLOUD_SERVER) + .caption("myMS-2") .resources(ofEntries( entry("CPUs", 2), entry("RAM", 10), entry("SSD", 50), - entry("Traffic", 2500) + entry("Traffic", 2500), + entry("SLA-Platform", "EXT4H"), + entry("SLA-EMail", true) )) .build() )) @@ -69,29 +78,42 @@ class HsPrivateCloudBookingItemValidatorUnitTest { final var privateCloudBookingItemEntity = HsBookingItemEntity.builder() .project(project) .type(PRIVATE_CLOUD) + .caption("myPC") .resources(ofEntries( entry("CPUs", 4), entry("RAM", 20), entry("SSD", 100), - entry("Traffic", 5000) + entry("Traffic", 5000), + entry("SLA-Platform EXT2H", 1), + entry("SLA-EMail", 1) )) .subBookingItems(of( HsBookingItemEntity.builder() .type(MANAGED_SERVER) + .caption("myMS-1") .resources(ofEntries( entry("CPUs", 3), entry("RAM", 20), entry("SSD", 100), - entry("Traffic", 3000) + entry("Traffic", 3000), + entry("SLA-Platform", "EXT2H"), + entry("SLA-EMail", true) )) .build(), HsBookingItemEntity.builder() .type(CLOUD_SERVER) + .caption("myMS-2") .resources(ofEntries( entry("CPUs", 2), entry("RAM", 10), entry("SSD", 50), - entry("Traffic", 2500) + entry("Traffic", 2500), + entry("SLA-Platform", "EXT2H"), + entry("SLA-EMail", true), + entry("SLA-Maria", true), + entry("SLA-PgSQL", true), + entry("SLA-Office", true), + entry("SLA-Web", true) )) .build() )) @@ -102,11 +124,16 @@ class HsPrivateCloudBookingItemValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( - "'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs 5", - "'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB", - "'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB", - "'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB" - ); + "'D-12345:Test-Project:myPC.resources.CPUs' maximum total is 4, but actual total CPUs is 5", + "'D-12345:Test-Project:myPC.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB", + "'D-12345:Test-Project:myPC.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB", + "'D-12345:Test-Project:myPC.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB", + "'D-12345:Test-Project:myPC.resources.SLA-Platform EXT2H maximum total is 1, but actual total for SLA-Platform=EXT2H is 2", + "'D-12345:Test-Project:myPC.resources.SLA-EMail' maximum total is 1, but actual total SLA-EMail is 2", + "'D-12345:Test-Project:myPC.resources.SLA-Maria' maximum total is 0, but actual total SLA-Maria is 1", + "'D-12345:Test-Project:myPC.resources.SLA-PgSQL' maximum total is 0, but actual total SLA-PgSQL is 1", + "'D-12345:Test-Project:myPC.resources.SLA-Office' maximum total is 0, but actual total SLA-Office is 1", + "'D-12345:Test-Project:myPC.resources.SLA-Web' maximum total is 0, but actual total SLA-Web is 1" + ); } - } 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 2ea554c6..84fe1627 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 @@ -10,8 +10,11 @@ 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; +import org.junit.jupiter.api.ClassOrderer; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestClassOrder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; @@ -28,11 +31,12 @@ import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.matchesRegex; +@Transactional @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = { HsadminNgApplication.class, JpaAttempt.class } ) -@Transactional +@TestClassOrder(ClassOrderer.OrderAnnotation.class) // fail early on fetching problems class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup { @LocalServerPort @@ -54,6 +58,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup JpaAttempt jpaAttempt; @Nested + @Order(2) class ListAssets { @Test @@ -152,6 +157,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Nested + @Order(3) class AddAsset { @Test @@ -231,17 +237,17 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .when() .post("http://localhost/api/hs/hosting/assets") .then().log().all().assertThat() - .statusCode(201) - .contentType(ContentType.JSON) - .body("", lenientlyEquals(""" - { - "type": "MANAGED_WEBSPACE", - "identifier": "fir90", - "caption": "some new ManagedWebspace in client's ManagedServer", - "config": {} - } - """)) - .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*")) + .statusCode(201) + .contentType(ContentType.JSON) + .body("", lenientlyEquals(""" + { + "type": "MANAGED_WEBSPACE", + "identifier": "fir90", + "caption": "some new ManagedWebspace in client's ManagedServer", + "config": {} + } + """)) + .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*")) .extract().header("Location"); // @formatter:on // finally, the new asset can be accessed under the generated UUID @@ -258,34 +264,33 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup RestAssured // @formatter:off .given() - .header("current-user", "superuser-alex@hostsharing.net") - .contentType(ContentType.JSON) - .body(""" - { - "bookingItemUuid": "%s", - "type": "MANAGED_SERVER", - "identifier": "vm1400", - "caption": "some new ManagedServer", - "config": { "monit_max_ssd_usage": 0, "monit_max_cpu_usage": 101, "extra": 42 } - } - """.formatted(givenBookingItem.getUuid())) - .port(port) + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "bookingItemUuid": "%s", + "type": "MANAGED_SERVER", + "identifier": "vm1400", + "caption": "some new ManagedServer", + "config": { "monit_max_ssd_usage": 0, "monit_max_cpu_usage": 101, "extra": 42 } + } + """.formatted(givenBookingItem.getUuid())) + .port(port) .when() - .post("http://localhost/api/hs/hosting/assets") + .post("http://localhost/api/hs/hosting/assets") .then().log().all().assertThat() - .statusCode(400) - .contentType(ContentType.JSON) - .body("", lenientlyEquals(""" - { - "statusPhrase": "Bad Request", - "message": "[ - <<<'MANAGED_SERVER:vm1400.config.extra' is not expected but is set to '42', - <<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be >= 10 but is 0, - <<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be <= 100 but is 101, - <<<'MANAGED_SERVER:vm1400.config.monit_max_ram_usage' is required but missing - <<<]" - } - """.replaceAll(" +<<<", ""))); // @formatter:on + .statusCode(400) + .contentType(ContentType.JSON) + .body("", lenientlyEquals(""" + { + "statusPhrase": "Bad Request", + "message": "[ + <<<'MANAGED_SERVER:vm1400.config.extra' is not expected but is set to '42', + <<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be <= 100 but is 101, + <<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be >= 10 but is 0 + <<<]" + } + """.replaceAll(" +<<<", ""))); // @formatter:on } @@ -333,15 +338,14 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .body("", lenientlyEquals(""" { "statusPhrase": "Bad Request", - "message": "[ - <<<'D-1000111:D-1000111 default project:separate ManagedWebspace.resources.Multi=1 allows at maximum 25 unix users, but 26 found - <<<]" + "message": "['D-1000111:D-1000111 default project:separate ManagedWebspace.resources.Multi=1 allows at maximum 25 unix users, but 26 found]" } """.replaceAll(" +<<<", ""))); // @formatter:on } } @Nested + @Order(1) class GetAsset { @Test @@ -413,6 +417,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Nested + @Order(4) class PatchAsset { @Test @@ -466,6 +471,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Nested + @Order(5) class DeleteAsset { @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java index e8195eeb..9a04c9b4 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java @@ -55,18 +55,22 @@ class HsHostingAssetPropsControllerAcceptanceTest { [ { "type": "integer", - "propertyName": "monit_min_free_ssd", - "min": 1, - "max": 1000, + "propertyName": "monit_max_cpu_usage", + "unit": "%", + "min": 10, + "max": 100, "required": false, + "defaultValue": 92, "isTotalsValidator": false }, { "type": "integer", - "propertyName": "monit_min_free_hdd", - "min": 1, - "max": 4000, + "propertyName": "monit_max_ram_usage", + "unit": "%", + "min": 10, + "max": 100, "required": false, + "defaultValue": 92, "isTotalsValidator": false }, { @@ -75,7 +79,17 @@ class HsHostingAssetPropsControllerAcceptanceTest { "unit": "%", "min": 10, "max": 100, - "required": true, + "required": false, + "defaultValue": 98, + "isTotalsValidator": false + }, + { + "type": "integer", + "propertyName": "monit_min_free_ssd", + "min": 1, + "max": 1000, + "required": false, + "defaultValue": 5, "isTotalsValidator": false }, { @@ -85,29 +99,157 @@ class HsHostingAssetPropsControllerAcceptanceTest { "min": 10, "max": 100, "required": false, + "defaultValue": 95, "isTotalsValidator": false }, { "type": "integer", - "propertyName": "monit_max_cpu_usage", - "unit": "%", - "min": 10, - "max": 100, - "required": true, + "propertyName": "monit_min_free_hdd", + "min": 1, + "max": 4000, + "required": false, + "defaultValue": 10, "isTotalsValidator": false }, { - "type": "integer", - "propertyName": "monit_max_ram_usage", - "unit": "%", - "min": 10, - "max": 100, - "required": true, + "type": "boolean", + "propertyName": "software-pgsql", + "required": false, + "defaultValue": true, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-mariadb", + "required": false, + "defaultValue": true, + "isTotalsValidator": false + }, + { + "type": "enumeration", + "propertyName": "php-default", + "values": [ + "5.6", + "7.0", + "7.1", + "7.2", + "7.3", + "7.4", + "8.0", + "8.1", + "8.2" + ], + "required": false, + "defaultValue": "8.2", + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-php-5.6", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-php-7.0", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-php-7.1", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-php-7.2", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-php-7.3", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-php-7.4", + "required": false, + "defaultValue": true, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-php-8.0", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-php-8.1", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-php-8.2", + "required": false, + "defaultValue": true, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-postfix-tls-1.0", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-dovecot-tls-1.0", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-clamav", + "required": false, + "defaultValue": true, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-collabora", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-libreoffice", + "required": false, + "defaultValue": false, + "isTotalsValidator": false + }, + { + "type": "boolean", + "propertyName": "software-imagemagick-ghostscript", + "required": false, + "defaultValue": false, "isTotalsValidator": false } ] """)); // @formatter:on } - } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java index b92e5dc9..ddceba8e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java @@ -3,7 +3,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import org.junit.jupiter.api.Test; -import jakarta.validation.ValidationException; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; import static org.assertj.core.api.Assertions.assertThat; @@ -23,10 +22,6 @@ class HsHostingAssetEntityValidatorUnitTest { final var result = catchThrowable( ()-> HsHostingAssetEntityValidatorRegistry.validated(managedServerHostingAssetEntity)); // then - assertThat(result).isInstanceOf(ValidationException.class) - .hasMessageContaining( - "'MANAGED_SERVER:vm1234.config.monit_max_ssd_usage' is required but missing", - "'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is required but missing", - "'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is required but missing"); + assertThat(result).isNull(); // all required properties have defaults } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java index b8e75436..d22ef590 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java @@ -32,7 +32,6 @@ class HsManagedServerHostingAssetValidatorUnitTest { assertThat(result).containsExactlyInAnyOrder( "'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be >= 10 but is 2", "'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be <= 100 but is 101", - "'MANAGED_SERVER:vm1234.config.monit_max_ssd_usage' is required but missing", "'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'"); } } 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 856356cf..dc1b3f61 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 @@ -109,9 +109,9 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean assertThat(debitorRepo.count()).isEqualTo(count + 1); } + @Transactional @ParameterizedTest @ValueSource(strings = {"", "a", "ab", "a12", "123", "12a"}) - @Transactional public void canNotCreateNewDebitorWithInvalidDefaultPrefix(final String givenPrefix) { // given context("superuser-alex@hostsharing.net"); diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java b/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java index 6c9ac849..5e9d8347 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/ContextBasedTestWithCleanup.java @@ -14,9 +14,12 @@ import org.junit.jupiter.api.TestInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; +import org.springframework.transaction.PlatformTransactionManager; import jakarta.persistence.*; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import static java.lang.System.out; import static java.util.Comparator.comparing; @@ -28,9 +31,13 @@ import static org.assertj.core.api.Assertions.assertThat; public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { private static final boolean DETAILED_BUT_SLOW_CHECK = true; + @PersistenceContext protected EntityManager em; + @Autowired + private PlatformTransactionManager tm; + @Autowired RbacGrantRepository rbacGrantRepo; @@ -166,12 +173,16 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { @AfterEach void cleanupAndCheckCleanup(final TestInfo testInfo) { - out.println(ContextBasedTestWithCleanup.class.getSimpleName() + ".cleanupAndCheckCleanup"); - cleanupTemporaryTestData(); - deleteLeakedRbacObjects(); - long rbacObjectCount = assertNoNewRbacObjectsRolesAndGrantsLeaked(); + // If the whole test method has its own transaction, cleanup makes no sense. + // If that transaction even failed, cleaunup would cause an exception. + if (!tm.getTransaction(null).isRollbackOnly()) { + out.println(ContextBasedTestWithCleanup.class.getSimpleName() + ".cleanupAndCheckCleanup"); + cleanupTemporaryTestData(); + repeatUntilTrue(3, this::deleteLeakedRbacObjects); - out.println("TOTAL OBJECT COUNT (after): " + rbacObjectCount); + long rbacObjectCount = assertNoNewRbacObjectsRolesAndGrantsLeaked(); + out.println("TOTAL OBJECT COUNT (after): " + rbacObjectCount); + } } private void cleanupTemporaryTestData() { @@ -218,7 +229,8 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { }).assertSuccessful().returnedValue(); } - private void deleteLeakedRbacObjects() { + private boolean deleteLeakedRbacObjects() { + final var deletionSuccessful = new AtomicBoolean(true); rbacObjectRepo.findAll().stream() .filter(o -> o.serialId > latestIntialTestDataSerialId) .sorted(comparing(o -> o.serialId)) @@ -235,8 +247,10 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { if (exception != null) { out.println("DELETING leaked " + o.objectTable + "#" + o.uuid + " FAILED " + exception); + deletionSuccessful.set(false); } }); + return deletionSuccessful.get(); } private void assertEqual(final Set before, final Set after) { @@ -297,6 +311,15 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { "doc/temp/" + name + ".md" ); } + + public static boolean repeatUntilTrue(int maxAttempts, Supplier method) { + for (int attempts = 0; attempts < maxAttempts; attempts++) { + if (method.get()) { + return true; + } + } + return false; + } } interface RbacObjectRepository extends Repository { From 9418303b7c8acfa3c29c33013b164c01aa8da10d Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 21 Jun 2024 12:02:07 +0200 Subject: [PATCH 08/18] add optional alarm-contact to hosting-asset (#64) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/64 Reviewed-by: Marc Sandlus --- .../asset/HsHostingAssetController.java | 7 +- .../hosting/asset/HsHostingAssetEntity.java | 17 +- .../asset/HsHostingAssetEntityPatcher.java | 12 +- .../hs-hosting/hs-hosting-asset-schemas.yaml | 10 + .../6303-hs-booking-item-rbac.md | 63 ++++ .../6303-hs-booking-item-rbac.sql | 277 ++++++++++++++++++ .../7010-hs-hosting-asset.sql | 1 + ...7013-hs-hosting-asset-rbac-CLOUD_SERVER.md | 72 ----- ...13-hs-hosting-asset-rbac-MANAGED_SERVER.md | 72 ----- ...-hs-hosting-asset-rbac-MANAGED_WEBSPACE.md | 73 ----- .../7013-hs-hosting-asset-rbac.md | 58 +++- .../7013-hs-hosting-asset-rbac.sql | 103 +++---- .../hsadminng/arch/ArchitectureTest.java | 7 +- ...sBookingItemRepositoryIntegrationTest.java | 6 +- ...sHostingAssetControllerAcceptanceTest.java | 26 +- .../HsHostingAssetEntityPatcherUnitTest.java | 25 +- ...HostingAssetRepositoryIntegrationTest.java | 1 + 17 files changed, 544 insertions(+), 286 deletions(-) create mode 100644 src/main/resources/db/changelog/6-hs-booking/630-booking-item/6303-hs-booking-item-rbac.md create mode 100644 src/main/resources/db/changelog/6-hs-booking/630-booking-item/6303-hs-booking-item-rbac.sql delete mode 100644 src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac-CLOUD_SERVER.md delete mode 100644 src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac-MANAGED_SERVER.md delete mode 100644 src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac-MANAGED_WEBSPACE.md 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 d3578833..b7982328 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 @@ -16,7 +16,9 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import jakarta.persistence.EntityManager; import jakarta.persistence.EntityNotFoundException; +import jakarta.persistence.PersistenceContext; import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; @@ -26,6 +28,9 @@ import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAss @RestController public class HsHostingAssetController implements HsHostingAssetsApi { + @PersistenceContext + private EntityManager em; + @Autowired private Context context; @@ -119,7 +124,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { final var current = assetRepo.findByUuid(assetUuid).orElseThrow(); - new HsHostingAssetEntityPatcher(current).apply(body); + new HsHostingAssetEntityPatcher(em, current).apply(body); final var saved = validated(assetRepo.save(current)); final var mapped = mapper.map(saved, HsHostingAssetResource.class); 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 fa15537a..76bcd40d 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 @@ -8,6 +8,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; import net.hostsharing.hsadminng.mapper.PatchableMapWrapper; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; @@ -48,6 +49,7 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.UPDATE; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN; 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.REFERRER; 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.rbacViewFor; @@ -95,6 +97,10 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject { @Enumerated(EnumType.STRING) private HsHostingAssetType type; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "alarmcontactuuid") + private HsOfficeContactEntity alarmContact; + @OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true, fetch = FetchType.LAZY) @JoinColumn(name="parentassetuuid", referencedColumnName="uuid") private List subHostingAssets; @@ -136,7 +142,7 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject { return rbacViewFor("asset", HsHostingAssetEntity.class) .withIdentityView(SQL.projection("identifier")) .withRestrictedViewOrderBy(SQL.expression("identifier")) - .withUpdatableColumns("version", "caption", "config") + .withUpdatableColumns("version", "caption", "config", "assignedToAssetUuid", "alarmContactUuid") .toRole(GLOBAL, ADMIN).grantPermission(INSERT) // TODO.impl: Why is this necessary to insert test data? .importEntityAlias("bookingItem", HsBookingItemEntity.class, usingDefaultCase(), @@ -155,6 +161,11 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject { directlyFetchedByDependsOnColumn(), NULLABLE) + .importEntityAlias("alarmContact", HsOfficeContactEntity.class, usingDefaultCase(), + dependsOnColumn("alarmContactUuid"), + directlyFetchedByDependsOnColumn(), + NULLABLE) + .createRole(OWNER, (with) -> { with.incomingSuperRole("bookingItem", ADMIN); with.incomingSuperRole("parentAsset", ADMIN); @@ -167,13 +178,15 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject { }) .createSubRole(AGENT, (with) -> { with.outgoingSubRole("assignedToAsset", TENANT); + with.outgoingSubRole("alarmContact", REFERRER); }) .createSubRole(TENANT, (with) -> { with.outgoingSubRole("bookingItem", TENANT); with.outgoingSubRole("parentAsset", TENANT); + with.incomingSuperRole("alarmContact", ADMIN); with.permission(SELECT); }) - .limitDiagramTo("asset", "bookingItem", "bookingItem.debitorRel", "parentAsset", "assignedToAsset", "global"); + .limitDiagramTo("asset", "bookingItem", "bookingItem.debitorRel", "parentAsset", "assignedToAsset", "alarmContact", "global"); } public static void main(String[] args) throws IOException { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcher.java index a555be19..f1cff713 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcher.java @@ -1,17 +1,21 @@ package net.hostsharing.hsadminng.hs.hosting.asset; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetPatchResource; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; import net.hostsharing.hsadminng.mapper.EntityPatcher; import net.hostsharing.hsadminng.mapper.KeyValueMap; import net.hostsharing.hsadminng.mapper.OptionalFromJson; +import jakarta.persistence.EntityManager; import java.util.Optional; public class HsHostingAssetEntityPatcher implements EntityPatcher { + private final EntityManager em; private final HsHostingAssetEntity entity; - public HsHostingAssetEntityPatcher(final HsHostingAssetEntity entity) { + HsHostingAssetEntityPatcher(final EntityManager em, final HsHostingAssetEntity entity) { + this.em = em; this.entity = entity; } @@ -21,5 +25,11 @@ public class HsHostingAssetEntityPatcher implements EntityPatcher entity.getConfig().patch(KeyValueMap.from(resource.getConfig()))); + OptionalFromJson.of(resource.getAlarmContactUuid()) + // HOWTO: patch nullable JSON resource uuid to an ntity reference + .ifPresent(newValue -> entity.setAlarmContact( + Optional.ofNullable(newValue) + .map(uuid -> em.getReference(HsOfficeContactEntity.class, newValue)) + .orElse(null))); } } diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml index 8e9dbe02..934c9647 100644 --- a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml @@ -32,6 +32,8 @@ components: type: string caption: type: string + alarmContact: + $ref: '../hs-office/hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact' config: $ref: '#/components/schemas/HsHostingAssetConfiguration' required: @@ -46,6 +48,10 @@ components: caption: type: string nullable: true + alarmContactUuid: + type: string + format: uuid + nullable: true config: $ref: '#/components/schemas/HsHostingAssetConfiguration' @@ -72,6 +78,10 @@ components: minLength: 3 maxLength: 80 nullable: false + alarmContactUuid: + type: string + format: uuid + nullable: true config: $ref: '#/components/schemas/HsHostingAssetConfiguration' required: diff --git a/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6303-hs-booking-item-rbac.md b/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6303-hs-booking-item-rbac.md new file mode 100644 index 00000000..4775616f --- /dev/null +++ b/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6303-hs-booking-item-rbac.md @@ -0,0 +1,63 @@ +### rbac bookingItem + +This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually. + +```mermaid +%%{init:{'flowchart':{'htmlLabels':false}}}%% +flowchart TB + +subgraph bookingItem["`**bookingItem**`"] + direction TB + style bookingItem fill:#dd4901,stroke:#274d6e,stroke-width:8px + + subgraph bookingItem:roles[ ] + style bookingItem:roles fill:#dd4901,stroke:white + + role:bookingItem:OWNER[[bookingItem:OWNER]] + role:bookingItem:ADMIN[[bookingItem:ADMIN]] + role:bookingItem:AGENT[[bookingItem:AGENT]] + role:bookingItem:TENANT[[bookingItem:TENANT]] + end + + subgraph bookingItem:permissions[ ] + style bookingItem:permissions fill:#dd4901,stroke:white + + perm:bookingItem:INSERT{{bookingItem:INSERT}} + perm:bookingItem:DELETE{{bookingItem:DELETE}} + perm:bookingItem:UPDATE{{bookingItem:UPDATE}} + perm:bookingItem:SELECT{{bookingItem:SELECT}} + end +end + +subgraph project["`**project**`"] + direction TB + style project fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph project:roles[ ] + style project:roles fill:#99bcdb,stroke:white + + 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: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:bookingItem:ADMIN ==> role:bookingItem:AGENT +role:bookingItem:AGENT ==> role:bookingItem:TENANT +role:bookingItem:TENANT ==> role:project:TENANT + +%% granting permissions to roles +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/630-booking-item/6303-hs-booking-item-rbac.sql b/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6303-hs-booking-item-rbac.sql new file mode 100644 index 00000000..bcd6523e --- /dev/null +++ b/src/main/resources/db/changelog/6-hs-booking/630-booking-item/6303-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/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 7e96a3fd..bd6ff6e4 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 @@ -33,6 +33,7 @@ create table if not exists hs_hosting_asset identifier varchar(80) not null, caption varchar(80), config jsonb not null, + alarmContactUuid uuid null references hs_office_contact(uuid) initially deferred, 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 deleted file mode 100644 index c4abe818..00000000 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac-CLOUD_SERVER.md +++ /dev/null @@ -1,72 +0,0 @@ -### rbac asset inCaseOf:CLOUD_SERVER - -This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually. - -```mermaid -%%{init:{'flowchart':{'htmlLabels':false}}}%% -flowchart TB - -subgraph asset["`**asset**`"] - direction TB - style asset fill:#dd4901,stroke:#274d6e,stroke-width:8px - - subgraph asset:roles[ ] - style asset:roles fill:#dd4901,stroke:white - - role:asset:OWNER[[asset:OWNER]] - role:asset:ADMIN[[asset:ADMIN]] - role:asset:TENANT[[asset:TENANT]] - end - - subgraph asset:permissions[ ] - style asset:permissions fill:#dd4901,stroke:white - - perm:asset:INSERT{{asset:INSERT}} - perm:asset:DELETE{{asset:DELETE}} - perm:asset:UPDATE{{asset:UPDATE}} - perm:asset:SELECT{{asset:SELECT}} - end -end - -subgraph bookingItem["`**bookingItem**`"] - direction TB - style bookingItem fill:#99bcdb,stroke:#274d6e,stroke-width:8px - - subgraph bookingItem:roles[ ] - style bookingItem:roles fill:#99bcdb,stroke:white - - role:bookingItem:OWNER[[bookingItem:OWNER]] - role:bookingItem:ADMIN[[bookingItem:ADMIN]] - role:bookingItem:AGENT[[bookingItem:AGENT]] - role:bookingItem:TENANT[[bookingItem: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:bookingItem:OWNER -.-> role:bookingItem:ADMIN -role:bookingItem:ADMIN -.-> role:bookingItem:AGENT -role:bookingItem:AGENT -.-> role:bookingItem:TENANT -role:bookingItem:ADMIN ==> role:asset:OWNER -role:asset:OWNER ==> role:asset:ADMIN -role:asset:ADMIN ==> role:asset:TENANT -role:asset:TENANT ==> role:bookingItem:TENANT - -%% granting permissions to roles -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 deleted file mode 100644 index 5d9b4710..00000000 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac-MANAGED_SERVER.md +++ /dev/null @@ -1,72 +0,0 @@ -### rbac asset inCaseOf:MANAGED_SERVER - -This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually. - -```mermaid -%%{init:{'flowchart':{'htmlLabels':false}}}%% -flowchart TB - -subgraph asset["`**asset**`"] - direction TB - style asset fill:#dd4901,stroke:#274d6e,stroke-width:8px - - subgraph asset:roles[ ] - style asset:roles fill:#dd4901,stroke:white - - role:asset:OWNER[[asset:OWNER]] - role:asset:ADMIN[[asset:ADMIN]] - role:asset:TENANT[[asset:TENANT]] - end - - subgraph asset:permissions[ ] - style asset:permissions fill:#dd4901,stroke:white - - perm:asset:INSERT{{asset:INSERT}} - perm:asset:DELETE{{asset:DELETE}} - perm:asset:UPDATE{{asset:UPDATE}} - perm:asset:SELECT{{asset:SELECT}} - end -end - -subgraph bookingItem["`**bookingItem**`"] - direction TB - style bookingItem fill:#99bcdb,stroke:#274d6e,stroke-width:8px - - subgraph bookingItem:roles[ ] - style bookingItem:roles fill:#99bcdb,stroke:white - - role:bookingItem:OWNER[[bookingItem:OWNER]] - role:bookingItem:ADMIN[[bookingItem:ADMIN]] - role:bookingItem:AGENT[[bookingItem:AGENT]] - role:bookingItem:TENANT[[bookingItem: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:bookingItem:OWNER -.-> role:bookingItem:ADMIN -role:bookingItem:ADMIN -.-> role:bookingItem:AGENT -role:bookingItem:AGENT -.-> role:bookingItem:TENANT -role:bookingItem:ADMIN ==> role:asset:OWNER -role:asset:OWNER ==> role:asset:ADMIN -role:asset:ADMIN ==> role:asset:TENANT -role:asset:TENANT ==> role:bookingItem:TENANT - -%% granting permissions to roles -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 deleted file mode 100644 index 5a35b108..00000000 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac-MANAGED_WEBSPACE.md +++ /dev/null @@ -1,73 +0,0 @@ -### rbac asset inCaseOf:MANAGED_WEBSPACE - -This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually. - -```mermaid -%%{init:{'flowchart':{'htmlLabels':false}}}%% -flowchart TB - -subgraph asset["`**asset**`"] - direction TB - style asset fill:#dd4901,stroke:#274d6e,stroke-width:8px - - subgraph asset:roles[ ] - style asset:roles fill:#dd4901,stroke:white - - role:asset:OWNER[[asset:OWNER]] - role:asset:ADMIN[[asset:ADMIN]] - role:asset:TENANT[[asset:TENANT]] - end - - subgraph asset:permissions[ ] - style asset:permissions fill:#dd4901,stroke:white - - perm:asset:INSERT{{asset:INSERT}} - perm:asset:DELETE{{asset:DELETE}} - perm:asset:UPDATE{{asset:UPDATE}} - perm:asset:SELECT{{asset:SELECT}} - end -end - -subgraph bookingItem["`**bookingItem**`"] - direction TB - style bookingItem fill:#99bcdb,stroke:#274d6e,stroke-width:8px - - subgraph bookingItem:roles[ ] - style bookingItem:roles fill:#99bcdb,stroke:white - - role:bookingItem:OWNER[[bookingItem:OWNER]] - role:bookingItem:ADMIN[[bookingItem:ADMIN]] - role:bookingItem:AGENT[[bookingItem:AGENT]] - role:bookingItem:TENANT[[bookingItem: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:bookingItem:OWNER -.-> role:bookingItem:ADMIN -role:bookingItem:ADMIN -.-> role:bookingItem:AGENT -role:bookingItem:AGENT -.-> role:bookingItem:TENANT -role:bookingItem:ADMIN ==> role:asset:OWNER -role:asset:OWNER ==> role:asset:ADMIN -role:asset:ADMIN ==> role:asset:TENANT -role:asset:TENANT ==> role:bookingItem:TENANT - -%% granting permissions to roles -role:bookingItem:AGENT ==> perm:asset:INSERT -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 bf7780e1..f0b250db 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 @@ -6,6 +6,19 @@ This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manua %%{init:{'flowchart':{'htmlLabels':false}}}%% flowchart TB +subgraph alarmContact["`**alarmContact**`"] + direction TB + style alarmContact fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph alarmContact:roles[ ] + style alarmContact:roles fill:#99bcdb,stroke:white + + role:alarmContact:OWNER[[alarmContact:OWNER]] + role:alarmContact:ADMIN[[alarmContact:ADMIN]] + role:alarmContact:REFERRER[[alarmContact:REFERRER]] + end +end + subgraph asset["`**asset**`"] direction TB style asset fill:#dd4901,stroke:#274d6e,stroke-width:8px @@ -25,6 +38,7 @@ subgraph asset["`**asset**`"] perm:asset:INSERT{{asset:INSERT}} perm:asset:DELETE{{asset:DELETE}} perm:asset:UPDATE{{asset:UPDATE}} + perm:asset:SELECT{{asset:SELECT}} end end @@ -39,16 +53,58 @@ subgraph assignedToAsset["`**assignedToAsset**`"] end end +subgraph bookingItem["`**bookingItem**`"] + direction TB + style bookingItem fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph bookingItem:roles[ ] + style bookingItem:roles fill:#99bcdb,stroke:white + + role:bookingItem:OWNER[[bookingItem:OWNER]] + role:bookingItem:ADMIN[[bookingItem:ADMIN]] + role:bookingItem:AGENT[[bookingItem:AGENT]] + role:bookingItem:TENANT[[bookingItem:TENANT]] + end +end + +subgraph parentAsset["`**parentAsset**`"] + direction TB + style parentAsset fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph parentAsset:roles[ ] + style parentAsset:roles fill:#99bcdb,stroke:white + + role:parentAsset:ADMIN[[parentAsset:ADMIN]] + role:parentAsset:AGENT[[parentAsset:AGENT]] + role:parentAsset:TENANT[[parentAsset:TENANT]] + end +end + %% granting roles to roles +role:bookingItem:OWNER -.-> role:bookingItem:ADMIN +role:bookingItem:ADMIN -.-> role:bookingItem:AGENT +role:bookingItem:AGENT -.-> role:bookingItem:TENANT +role:global:ADMIN -.-> role:alarmContact:OWNER +role:alarmContact:OWNER -.-> role:alarmContact:ADMIN +role:alarmContact:ADMIN -.-> role:alarmContact:REFERRER +role:bookingItem:ADMIN ==> role:asset:OWNER +role:parentAsset:ADMIN ==> role:asset:OWNER role:asset:OWNER ==> role:asset:ADMIN +role:bookingItem:AGENT ==> role:asset:ADMIN +role:parentAsset:AGENT ==> role:asset:ADMIN role:asset:ADMIN ==> role:asset:AGENT role:asset:AGENT ==> role:assignedToAsset:TENANT +role:asset:AGENT ==> role:alarmContact:REFERRER role:asset:AGENT ==> role:asset:TENANT -role:assignedToAsset:TENANT ==> role:asset:TENANT +role:asset:TENANT ==> role:bookingItem:TENANT +role:asset:TENANT ==> role:parentAsset:TENANT +role:alarmContact:ADMIN ==> role:asset:TENANT %% granting permissions to roles role:global:ADMIN ==> perm:asset:INSERT +role:parentAsset:ADMIN ==> 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 f14430a7..cbaffa47 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 @@ -32,6 +32,7 @@ create or replace procedure buildRbacSystemForHsHostingAsset( declare newBookingItem hs_booking_item; newAssignedToAsset hs_hosting_asset; + newAlarmContact hs_office_contact; newParentAsset hs_hosting_asset; begin @@ -41,6 +42,8 @@ begin SELECT * FROM hs_hosting_asset WHERE uuid = NEW.assignedToAssetUuid INTO newAssignedToAsset; + SELECT * FROM hs_office_contact WHERE uuid = NEW.alarmContactUuid INTO newAlarmContact; + SELECT * FROM hs_hosting_asset WHERE uuid = NEW.parentAssetUuid INTO newParentAsset; perform createRoleWithGrants( @@ -63,14 +66,17 @@ begin perform createRoleWithGrants( hsHostingAssetAGENT(NEW), incomingSuperRoles => array[hsHostingAssetADMIN(NEW)], - outgoingSubRoles => array[hsHostingAssetTENANT(newAssignedToAsset)] + outgoingSubRoles => array[ + hsHostingAssetTENANT(newAssignedToAsset), + hsOfficeContactREFERRER(newAlarmContact)] ); perform createRoleWithGrants( hsHostingAssetTENANT(NEW), + permissions => array['SELECT'], incomingSuperRoles => array[ hsHostingAssetAGENT(NEW), - hsHostingAssetTENANT(newAssignedToAsset)], + hsOfficeContactADMIN(newAlarmContact)], outgoingSubRoles => array[ hsBookingItemTENANT(newBookingItem), hsHostingAssetTENANT(newParentAsset)] @@ -99,6 +105,48 @@ execute procedure insertTriggerForHsHostingAsset_tf(); --// +-- ============================================================================ +--changeset hs-hosting-asset-rbac-update-trigger:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/* + Called from the AFTER UPDATE TRIGGER to re-wire the grants. + */ + +create or replace procedure updateRbacRulesForHsHostingAsset( + OLD hs_hosting_asset, + NEW hs_hosting_asset +) + language plpgsql as $$ +begin + + if NEW.assignedToAssetUuid is distinct from OLD.assignedToAssetUuid + or NEW.alarmContactUuid is distinct from OLD.alarmContactUuid then + delete from rbacgrants g where g.grantedbytriggerof = OLD.uuid; + call buildRbacSystemForHsHostingAsset(NEW); + end if; +end; $$; + +/* + AFTER INSERT TRIGGER to re-wire the grant structure for a new hs_hosting_asset row. + */ + +create or replace function updateTriggerForHsHostingAsset_tf() + returns trigger + language plpgsql + strict as $$ +begin + call updateRbacRulesForHsHostingAsset(OLD, NEW); + return NEW; +end; $$; + +create trigger updateTriggerForHsHostingAsset_tg + after update on hs_hosting_asset + for each row +execute procedure updateTriggerForHsHostingAsset_tf(); +--// + + -- ============================================================================ --changeset hs-hosting-asset-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--// -- ---------------------------------------------------------------------------- @@ -146,49 +194,6 @@ create trigger z_new_hs_hosting_asset_grants_insert_to_global_tg for each row execute procedure new_hs_hosting_asset_grants_insert_to_global_tf(); --- granting INSERT permission to hs_booking_item ---------------------------- - -/* - Grants INSERT INTO hs_hosting_asset permissions to specified role of pre-existing hs_booking_item rows. - */ -do language plpgsql $$ - declare - row hs_booking_item; - begin - call defineContext('create INSERT INTO hs_hosting_asset permissions for pre-exising hs_booking_item rows'); - - FOR row IN SELECT * FROM hs_booking_item - -- unconditional for all rows in that table - LOOP - call grantPermissionToRole( - createPermission(row.uuid, 'INSERT', 'hs_hosting_asset'), - hsBookingItemAGENT(row)); - END LOOP; - end; -$$; - -/** - Grants hs_hosting_asset INSERT permission to specified role of new hs_booking_item rows. -*/ -create or replace function new_hs_hosting_asset_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_hosting_asset'), - hsBookingItemAGENT(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_hosting_asset_grants_insert_to_hs_booking_item_tg - after insert on hs_booking_item - for each row -execute procedure new_hs_hosting_asset_grants_insert_to_hs_booking_item_tf(); - -- granting INSERT permission to hs_hosting_asset ---------------------------- -- Granting INSERT INTO hs_hosting_asset permissions to specified role of pre-existing hs_hosting_asset rows slipped, @@ -234,10 +239,6 @@ begin if isGlobalAdmin() then return NEW; end if; - -- check INSERT permission via direct foreign key: NEW.bookingItemUuid - if hasInsertPermission(NEW.bookingItemUuid, 'hs_hosting_asset') then - return NEW; - end if; -- check INSERT permission via direct foreign key: NEW.parentAssetUuid if hasInsertPermission(NEW.parentAssetUuid, 'hs_hosting_asset') then return NEW; @@ -275,7 +276,9 @@ call generateRbacRestrictedView('hs_hosting_asset', $updates$ version = new.version, caption = new.caption, - config = new.config + config = new.config, + assignedToAssetUuid = new.assignedToAssetUuid, + alarmContactUuid = new.alarmContactUuid $updates$); --// diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index df26279d..f626a3ed 100644 --- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java +++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java @@ -150,7 +150,8 @@ public class ArchitectureTest { .should().onlyBeAccessed().byClassesThat() .resideInAnyPackage( "..hs.booking.(*)..", - "..hs.hosting.(*).." + "..hs.hosting.(*)..", + "..hs.validation" // TODO.impl: Some Validators need to be refactored to booking package. ); @ArchTest @@ -195,7 +196,9 @@ public class ArchitectureTest { "..hs.office.partner..", "..hs.office.debitor..", "..hs.office.membership..", - "..hs.office.migration.."); + "..hs.office.migration..", + "..hs.hosting.asset.." + ); @ArchTest @SuppressWarnings("unused") 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 b474d0c7..f125974a 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 @@ -97,9 +97,7 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // 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(); + final var initialGrantNames = distinctGrantDisplaysOf(rawGrantRepo.findAll()); // when attempt(em, () -> { @@ -124,7 +122,6 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup "hs_booking_item#somenewbookingitem:OWNER", "hs_booking_item#somenewbookingitem:TENANT")); assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())) - .map(s -> s.replace("hs_office_", "")) .containsExactlyInAnyOrder(fromFormatted( initialGrantNames, @@ -138,7 +135,6 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // admin "{ 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#somenewbookingitem:AGENT to role:hs_booking_item#somenewbookingitem:ADMIN by system and assume }", 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 84fe1627..44f5327f 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 @@ -7,6 +7,8 @@ import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRepository; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; @@ -54,6 +56,9 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup @Autowired HsOfficeDebitorRepository debitorRepo; + @Autowired + HsOfficeContactRepository contactRepo; + @Autowired JpaAttempt jpaAttempt; @@ -425,6 +430,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup final var givenAsset = givenSomeTemporaryHostingAsset("2001", MANAGED_SERVER, config("monit_max_ssd_usage", 80), config("monit_max_hdd_usage", 90), config("monit_max_cpu_usage", 90), config("monit_max_ram_usage", 70)); + final var alarmContactUuid = givenContact().getUuid(); RestAssured // @formatter:off .given() @@ -432,13 +438,14 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .contentType(ContentType.JSON) .body(""" { + "alarmContactUuid": "%s", "config": { "monit_max_ssd_usage": 85, "monit_max_hdd_usage": null, "monit_min_free_ssd": 5 } } - """) + """.formatted(alarmContactUuid)) .port(port) .when() .patch("http://localhost/api/hs/hosting/assets/" + givenAsset.getUuid()) @@ -450,6 +457,11 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "type": "MANAGED_SERVER", "identifier": "vm2001", "caption": "some test-asset", + "alarmContact": { + "uuid": "%s", + "caption": "second contact", + "emailAddresses": { "main": "contact-admin@secondcontact.example.com" } + }, "config": { "monit_max_cpu_usage": 90, "monit_max_ram_usage": 70, @@ -457,12 +469,15 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "monit_min_free_ssd": 5 } } - """)); // @formatter:on + """.formatted(alarmContactUuid))); + // @formatter:on // finally, the asset is actually updated context.define("superuser-alex@hostsharing.net"); assertThat(assetRepo.findByUuid(givenAsset.getUuid())).isPresent().get() .matches(asset -> { + assertThat(asset.getAlarmContact().toString()).isEqualTo( + "contact(caption='second contact', emailAddresses='{ main: contact-admin@secondcontact.example.com }')"); assertThat(asset.getConfig().toString()).isEqualTo( "{ monit_max_cpu_usage: 90, monit_max_ram_usage: 70, monit_max_ssd_usage: 85, monit_min_free_ssd: 5 }"); return true; @@ -470,6 +485,13 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } } + private HsOfficeContactEntity givenContact() { + return jpaAttempt.transacted(() -> { + context.define("superuser-alex@hostsharing.net"); + return contactRepo.findContactByOptionalCaptionLike("second").stream().findFirst().orElseThrow(); + }).returnedValue(); + } + @Nested @Order(5) class DeleteAsset { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java index 2530f5fa..890932b4 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetPatchResource; -import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; import net.hostsharing.hsadminng.mapper.KeyValueMap; import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; import org.junit.jupiter.api.BeforeEach; @@ -31,6 +31,7 @@ class HsHostingAssetEntityPatcherUnitTest extends PatchUnitTestBase< > { private static final UUID INITIAL_BOOKING_ITEM_UUID = UUID.randomUUID(); + private static final UUID PATCHED_CONTACT_UUID = UUID.randomUUID(); private static final Map INITIAL_CONFIG = patchMap( entry("CPU", 1), @@ -47,6 +48,9 @@ class HsHostingAssetEntityPatcherUnitTest extends PatchUnitTestBase< entry("SSD", 256), entry("MEM", 64) ); + final HsOfficeContactEntity givenInitialContact = HsOfficeContactEntity.builder() + .uuid(UUID.randomUUID()) + .build(); private static final String INITIAL_CAPTION = "initial caption"; private static final String PATCHED_CAPTION = "patched caption"; @@ -56,10 +60,10 @@ class HsHostingAssetEntityPatcherUnitTest extends PatchUnitTestBase< @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(HsHostingAssetEntity.class), any())).thenAnswer(invocation -> HsHostingAssetEntity.builder().uuid(invocation.getArgument(1)).build()); + lenient().when(em.getReference(eq(HsOfficeContactEntity.class), any())).thenAnswer(invocation -> + HsOfficeContactEntity.builder().uuid(invocation.getArgument(1)).build()); } @Override @@ -69,6 +73,7 @@ class HsHostingAssetEntityPatcherUnitTest extends PatchUnitTestBase< entity.setBookingItem(TEST_BOOKING_ITEM); entity.getConfig().putAll(KeyValueMap.from(INITIAL_CONFIG)); entity.setCaption(INITIAL_CAPTION); + entity.setAlarmContact(givenInitialContact); return entity; } @@ -79,7 +84,7 @@ class HsHostingAssetEntityPatcherUnitTest extends PatchUnitTestBase< @Override protected HsHostingAssetEntityPatcher createPatcher(final HsHostingAssetEntity server) { - return new HsHostingAssetEntityPatcher(server); + return new HsHostingAssetEntityPatcher(em, server); } @Override @@ -96,7 +101,17 @@ class HsHostingAssetEntityPatcherUnitTest extends PatchUnitTestBase< PATCH_CONFIG, HsHostingAssetEntity::putConfig, PATCHED_CONFIG) - .notNullable() + .notNullable(), + new JsonNullableProperty<>( + "alarmContact", + HsHostingAssetPatchResource::setAlarmContactUuid, + PATCHED_CONTACT_UUID, + HsHostingAssetEntity::setAlarmContact, + newContact(PATCHED_CONTACT_UUID)) ); } + + static HsOfficeContactEntity newContact(final UUID uuid) { + return HsOfficeContactEntity.builder().uuid(uuid).build(); + } } 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 f4abe06c..e5bc1605 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 @@ -147,6 +147,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu "{ grant role:hs_booking_item#fir01:TENANT to role:hs_hosting_asset#fir00:TENANT by system and assume }", "{ grant role:hs_hosting_asset#fir00:TENANT to role:hs_hosting_asset#fir00:AGENT by system and assume }", "{ grant role:hs_hosting_asset#vm1011:TENANT to role:hs_hosting_asset#fir00:TENANT by system and assume }", + "{ grant perm:hs_hosting_asset#fir00:SELECT to role:hs_hosting_asset#fir00:TENANT by system and assume }", null)); } From de88f1d842794ff88d682135a8128304184b02c5 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 24 Jun 2024 12:33:14 +0200 Subject: [PATCH 09/18] hosting-asset-validation-beyond-property-validators (#65) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/65 Reviewed-by: Timotheus Pokorra --- .../hs/booking/item/HsBookingItemEntity.java | 4 +- .../hosting/asset/HsHostingAssetEntity.java | 13 +- .../HsCloudServerHostingAssetValidator.java | 23 +++ .../HsHostingAssetEntityValidator.java | 170 +++++++++++++++++- ...HsHostingAssetEntityValidatorRegistry.java | 5 +- .../HsManagedServerHostingAssetValidator.java | 16 +- ...sManagedWebspaceHostingAssetValidator.java | 31 ++-- .../HsUnixUserHostingAssetValidator.java | 23 +++ .../hs/hosting/asset/validators/README.md | 40 +++++ .../hs/booking/item/TestHsBookingItem.java | 12 +- ...sHostingAssetControllerAcceptanceTest.java | 54 ++++-- .../HsHostingAssetEntityPatcherUnitTest.java | 4 +- .../asset/HsHostingAssetEntityUnitTest.java | 8 +- ...HostingAssetRepositoryIntegrationTest.java | 2 + ...udServerHostingAssetValidatorUnitTest.java | 66 ++++++- ...gAssetEntityValidatorRegistryUnitTest.java | 64 +++++++ ...HsHostingAssetEntityValidatorUnitTest.java | 10 +- ...edServerHostingAssetValidatorUnitTest.java | 47 +++++ ...WebspaceHostingAssetValidatorUnitTest.java | 72 +++++++- ...UnixUserHostingAssetValidatorUnitTest.java | 30 ++++ 20 files changed, 639 insertions(+), 55 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/README.md create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java 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 90774110..94b80984 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 @@ -34,6 +34,7 @@ import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import jakarta.persistence.Transient; import jakarta.persistence.Version; +import jakarta.validation.constraints.NotNull; import java.io.IOException; import java.time.LocalDate; import java.util.HashMap; @@ -60,8 +61,8 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetche import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.stringify.Stringify.stringify; -@Builder @Entity +@Builder(toBuilder = true) @Table(name = "hs_booking_item_rv") @Getter @Setter @@ -92,6 +93,7 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { @JoinColumn(name = "parentitemuuid") private HsBookingItemEntity parentItem; + @NotNull @Column(name = "type") @Enumerated(EnumType.STRING) private HsBookingItemType type; 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 76bcd40d..ff7bfd33 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 @@ -29,6 +29,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; +import jakarta.persistence.PostLoad; import jakarta.persistence.Table; import jakarta.persistence.Transient; import jakarta.persistence.Version; @@ -120,12 +121,20 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject { @Transient private PatchableMapWrapper configWrapper; + @Transient + private boolean isLoaded = false; + + @PostLoad + public void markAsLoaded() { + this.isLoaded = true; + } + public PatchableMapWrapper getConfig() { return PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config ); } - public void putConfig(Map newConfg) { - PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfg); + public void putConfig(Map newConfig) { + PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfig); } @Override diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidator.java new file mode 100644 index 00000000..9144189b --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidator.java @@ -0,0 +1,23 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; + +import java.util.regex.Pattern; + +class HsCloudServerHostingAssetValidator extends HsHostingAssetEntityValidator { + + HsCloudServerHostingAssetValidator() { + super( + BookingItem.mustBeOfType(HsBookingItemType.CLOUD_SERVER), + ParentAsset.mustBeNull(), + AssignedToAsset.mustBeNull(), + AlarmContact.isOptional(), + NO_EXTRA_PROPERTIES); + } + + @Override + protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { + return Pattern.compile("^vm[0-9][0-9][0-9][0-9]$"); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java index c452d378..15ea12df 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java @@ -1,35 +1,80 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; import net.hostsharing.hsadminng.hs.validation.ValidatableProperty; +import jakarta.validation.constraints.NotNull; +import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Stream; import static java.util.Arrays.stream; import static java.util.Collections.emptyList; import static java.util.Optional.ofNullable; -public class HsHostingAssetEntityValidator extends HsEntityValidator { +public abstract class HsHostingAssetEntityValidator extends HsEntityValidator { - public HsHostingAssetEntityValidator(final ValidatableProperty... properties) { + static final ValidatableProperty[] NO_EXTRA_PROPERTIES = new ValidatableProperty[0]; + + private final HsHostingAssetEntityValidator.BookingItem bookingItemValidation; + private final HsHostingAssetEntityValidator.ParentAsset parentAssetValidation; + private final HsHostingAssetEntityValidator.AssignedToAsset assignedToAssetValidation; + private final HsHostingAssetEntityValidator.AlarmContact alarmContactValidation; + + HsHostingAssetEntityValidator( + @NotNull final BookingItem bookingItemValidation, + @NotNull final ParentAsset parentAssetValidation, + @NotNull final AssignedToAsset assignedToAssetValidation, + @NotNull final AlarmContact alarmContactValidation, + final ValidatableProperty... properties) { super(properties); + this.bookingItemValidation = bookingItemValidation; + this.parentAssetValidation = parentAssetValidation; + this.assignedToAssetValidation = assignedToAssetValidation; + this.alarmContactValidation = alarmContactValidation; } - @Override public List validate(final HsHostingAssetEntity assetEntity) { return sequentiallyValidate( - () -> validateProperties(assetEntity), + () -> validateEntityReferences(assetEntity), + () -> validateIdentifierPattern(assetEntity), // might need proper parentAsset or billingItem () -> optionallyValidate(assetEntity.getBookingItem()), () -> optionallyValidate(assetEntity.getParentAsset()), () -> validateAgainstSubEntities(assetEntity) ); } + private List validateEntityReferences(final HsHostingAssetEntity assetEntity) { + return Stream.of( + validateReferencedEntity(assetEntity, "bookingItem", bookingItemValidation::validate), + validateReferencedEntity(assetEntity, "parentAsset", parentAssetValidation::validate), + validateReferencedEntity(assetEntity, "assignedToAsset", assignedToAssetValidation::validate), + validateReferencedEntity(assetEntity, "alarmContact", alarmContactValidation::validate), + validateProperties(assetEntity)) + .filter(Objects::nonNull) + .flatMap(List::stream) + .filter(Objects::nonNull) + .toList(); + } + + private List validateReferencedEntity( + final HsHostingAssetEntity assetEntity, + final String referenceFieldName, + final BiFunction> validator) { + return enrich(prefix(assetEntity.toShortString()), validator.apply(assetEntity, referenceFieldName)); + } + private List validateProperties(final HsHostingAssetEntity assetEntity) { return enrich(prefix(assetEntity.toShortString(), "config"), validateProperties(assetEntity.getConfig())); } @@ -57,6 +102,7 @@ public class HsHostingAssetEntityValidator extends HsEntityValidator propDef) { @@ -73,4 +119,120 @@ public class HsHostingAssetEntityValidator extends HsEntityValidator validateIdentifierPattern(final HsHostingAssetEntity assetEntity) { + final var expectedIdentifierPattern = identifierPattern(assetEntity); + if (assetEntity.getIdentifier() == null || + !expectedIdentifierPattern.matcher(assetEntity.getIdentifier()).matches()) { + return List.of("'identifier' expected to match '"+expectedIdentifierPattern+"', but is '" + assetEntity.getIdentifier() + "'"); + } + return Collections.emptyList(); + } + + protected abstract Pattern identifierPattern(HsHostingAssetEntity assetEntity); + + static abstract class ReferenceValidator { + + private final Policy policy; + private final T subEntityType; + private final Function subEntityGetter; + private final Function subEntityTypeGetter; + + public ReferenceValidator( + final Policy policy, + final T subEntityType, + final Function subEntityGetter, + final Function subEntityTypeGetter) { + this.policy = policy; + this.subEntityType = subEntityType; + this.subEntityGetter = subEntityGetter; + this.subEntityTypeGetter = subEntityTypeGetter; + } + + public ReferenceValidator( + final Policy policy, + final Function subEntityGetter) { + this.policy = policy; + this.subEntityType = null; + this.subEntityGetter = subEntityGetter; + this.subEntityTypeGetter = e -> null; + } + + enum Policy { + OPTIONAL, FORBIDDEN, REQUIRED + } + + List validate(final HsHostingAssetEntity assetEntity, final String referenceFieldName) { + + final var subEntity = subEntityGetter.apply(assetEntity); + if (policy == Policy.REQUIRED && subEntity == null) { + return List.of(referenceFieldName + "' must not be null but is null"); + } + if (policy == Policy.FORBIDDEN && subEntity != null) { + return List.of(referenceFieldName + "' must be null but is set to "+ assetEntity.getBookingItem().toShortString()); + } + final var subItemType = subEntity != null ? subEntityTypeGetter.apply(subEntity) : null; + if (subEntityType != null && subItemType != subEntityType) { + return List.of(referenceFieldName + "' must be of type " + subEntityType + " but is of type " + subItemType); + } + return emptyList(); + } + } + + static class BookingItem extends ReferenceValidator { + + BookingItem(final Policy policy, final HsBookingItemType bookingItemType) { + super(policy, bookingItemType, HsHostingAssetEntity::getBookingItem, HsBookingItemEntity::getType); + } + + static BookingItem mustBeNull() { + return new BookingItem(Policy.FORBIDDEN, null); + } + + static BookingItem mustBeOfType(final HsBookingItemType hsBookingItemType) { + return new BookingItem(Policy.REQUIRED, hsBookingItemType); + } + } + + static class ParentAsset extends ReferenceValidator { + + ParentAsset(final ReferenceValidator.Policy policy, final HsHostingAssetType parentAssetType) { + super(policy, parentAssetType, HsHostingAssetEntity::getParentAsset, HsHostingAssetEntity::getType); + } + + static ParentAsset mustBeNull() { + return new ParentAsset(Policy.FORBIDDEN, null); + } + + static ParentAsset mustBeOfType(final HsHostingAssetType hostingAssetType) { + return new ParentAsset(Policy.REQUIRED, hostingAssetType); + } + + static ParentAsset mustBeNullOrOfType(final HsHostingAssetType hostingAssetType) { + return new ParentAsset(Policy.OPTIONAL, hostingAssetType); + } + } + + static class AssignedToAsset extends ReferenceValidator { + + AssignedToAsset(final ReferenceValidator.Policy policy, final HsHostingAssetType assignedToAssetType) { + super(policy, assignedToAssetType, HsHostingAssetEntity::getAssignedToAsset, HsHostingAssetEntity::getType); + } + + static AssignedToAsset mustBeNull() { + return new AssignedToAsset(Policy.FORBIDDEN, null); + } + } + + static class AlarmContact extends ReferenceValidator> { + + AlarmContact(final ReferenceValidator.Policy policy) { + super(policy, HsHostingAssetEntity::getAlarmContact); + } + + static AlarmContact isOptional() { + return new AlarmContact(Policy.OPTIONAL); + } + } + } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java index a1cac8e0..1b9a5241 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java @@ -14,10 +14,11 @@ public class HsHostingAssetEntityValidatorRegistry { private static final Map, HsEntityValidator> validators = new HashMap<>(); static { - register(CLOUD_SERVER, new HsHostingAssetEntityValidator()); + // HOWTO: add (register) new HsHostingAssetType-specific validators + register(CLOUD_SERVER, new HsCloudServerHostingAssetValidator()); register(MANAGED_SERVER, new HsManagedServerHostingAssetValidator()); register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator()); - register(UNIX_USER, new HsHostingAssetEntityValidator()); + register(UNIX_USER, new HsUnixUserHostingAssetValidator()); } private static void register(final Enum type, final HsEntityValidator validator) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java index 00050010..362abf38 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java @@ -1,5 +1,10 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; + +import java.util.regex.Pattern; + import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty; import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty; import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; @@ -8,6 +13,11 @@ class HsManagedServerHostingAssetValidator extends HsHostingAssetEntityValidator public HsManagedServerHostingAssetValidator() { super( + BookingItem.mustBeOfType(HsBookingItemType.MANAGED_SERVER), + ParentAsset.mustBeNull(), // until we introduce a hosting asset for 'HOST' + AssignedToAsset.mustBeNull(), + AlarmContact.isOptional(), // hostmaster alert address is implicitly added + // monitoring integerProperty("monit_max_cpu_usage").unit("%").min(10).max(100).withDefault(92), integerProperty("monit_max_ram_usage").unit("%").min(10).max(100).withDefault(92), @@ -15,7 +25,6 @@ class HsManagedServerHostingAssetValidator extends HsHostingAssetEntityValidator integerProperty("monit_min_free_ssd").min(1).max(1000).withDefault(5), integerProperty("monit_max_hdd_usage").unit("%").min(10).max(100).withDefault(95), integerProperty("monit_min_free_hdd").min(1).max(4000).withDefault(10), - // stringProperty("monit_alarm_email").unit("GB").optional() TODO.impl: via Contact? // other settings // booleanProperty("fastcgi_small").withDefault(false), TODO.spec: clarify Salt-Grains @@ -45,4 +54,9 @@ class HsManagedServerHostingAssetValidator extends HsHostingAssetEntityValidator booleanProperty("software-imagemagick-ghostscript").withDefault(false) ); } + + @Override + protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { + return Pattern.compile("^vm[0-9][0-9][0-9][0-9]$"); + } } 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 19c9dc24..bffedf2f 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 @@ -1,29 +1,26 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; -import java.util.Collection; -import java.util.stream.Stream; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; +import java.util.regex.Pattern; class HsManagedWebspaceHostingAssetValidator extends HsHostingAssetEntityValidator { public HsManagedWebspaceHostingAssetValidator() { + super(BookingItem.mustBeOfType(HsBookingItemType.MANAGED_WEBSPACE), + ParentAsset.mustBeOfType(HsHostingAssetType.MANAGED_SERVER), // the (shared or private) ManagedServer + AssignedToAsset.mustBeNull(), + AlarmContact.isOptional(), // hostmaster alert address is implicitly added + NO_EXTRA_PROPERTIES); } @Override - public List validate(final HsHostingAssetEntity assetEntity) { - return Stream.of(validateIdentifierPattern(assetEntity), super.validate(assetEntity)) - .flatMap(Collection::stream) - .collect(Collectors.toList()); - } - - private static List validateIdentifierPattern(final HsHostingAssetEntity assetEntity) { - final var expectedIdentifierPattern = "^" + assetEntity.getParentAsset().getBookingItem().getProject().getDebitor().getDefaultPrefix() + "[0-9][0-9]$"; - if ( !assetEntity.getIdentifier().matches(expectedIdentifierPattern)) { - return List.of("'identifier' expected to match '"+expectedIdentifierPattern+"', but is '" + assetEntity.getIdentifier() + "'"); - } - return Collections.emptyList(); + protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { + final var prefixPattern = + !assetEntity.isLoaded() + ? assetEntity.getParentAsset().getBookingItem().getProject().getDebitor().getDefaultPrefix() + : "[a-z][a-z0-9][a-z0-9]"; + return Pattern.compile("^" + prefixPattern + "[0-9][0-9]$"); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java new file mode 100644 index 00000000..dfe222fc --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java @@ -0,0 +1,23 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; + +import java.util.regex.Pattern; + +class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator { + + HsUnixUserHostingAssetValidator() { + super(BookingItem.mustBeNull(), + ParentAsset.mustBeOfType(HsHostingAssetType.MANAGED_WEBSPACE), + AssignedToAsset.mustBeNull(), + AlarmContact.isOptional(), // TODO.spec: for quota notifications + NO_EXTRA_PROPERTIES); // TODO.spec: yet to be specified + } + + @Override + protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { + final var webspaceIdentifier = assetEntity.getParentAsset().getIdentifier(); + return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"-[a-z0-9]+$"); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/README.md b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/README.md new file mode 100644 index 00000000..52e03058 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/README.md @@ -0,0 +1,40 @@ +### HsHostingAssetEntity-Validation + +There is just a single `HsHostingAssetEntity` class for all types of hosting assets like Managed-Server, Managed-Webspace, Unix-Users, Databases etc. These are distinguished by `HsHostingAssetType HsHostingAssetEntity.type`. + +For each of these types, a distinct validator has to be +implemented as a subclass of `HsHostingAssetEntityValidator` which needs to be registered (see `HsHostingAssetEntityValidatorRegistry`) for the relevant type(s). + +### Kinds of Validations + +#### Identifier validation + +The identifier of a Hosting-Asset is for example the Webspace-Name like "xyz00" or a Unix-User-Name like "xyz00-test". + +To validate the identifier, vverride the method `identifierPattern(...)` and return a regular expression to validate the identifier against. The regular expression can depend on the actual entity instance. + +#### Reference validation + +References in this context are: +- the related Booking-Item, +- the parent-Hosting-Asset, +- the Assigned-To-Hosting-Asset and +- the Contact. + +The first parameters of the `HsHostingAssetEntityValidator` superclass take rule descriptors for these references. These are all Subclasses fo + +### Validation Order + +The validations are called in a sensible order. E.g. if a property value is not numeric, it makes no sense to check the total sum of such values to be within certain numeric values. And if the related booking item is of wrong type, it makes no sense to validate limits against sub-entities. + +Properties are validated all at once, though. Thus, if multiple properties fail validation, all error messages are returned at once. + +In general, the validation es executed in this order: + +1. the entity itself + 1. its references + 2. its properties +2. the limits of the parent entity (parent asset + booking item) +3. limits against the own own-sub-entities + +This implementation can be found in `HsHostingAssetEntityValidator.validate`. 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 00c0d706..0779fa2f 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 @@ -12,13 +12,21 @@ import static net.hostsharing.hsadminng.hs.booking.project.TestHsBookingProject. @UtilityClass public class TestHsBookingItem { - public static final HsBookingItemEntity TEST_BOOKING_ITEM = HsBookingItemEntity.builder() + public static final HsBookingItemEntity TEST_MANAGED_SERVER_BOOKING_ITEM = HsBookingItemEntity.builder() .project(TEST_PROJECT) - .caption("test booking item") + .type(HsBookingItemType.MANAGED_SERVER) + .caption("test project booking item") .resources(Map.ofEntries( entry("someThing", 1), entry("anotherThing", "blue") )) .validity(Range.closedInfinite(LocalDate.of(2020, 1, 15))) .build(); + + public static final HsBookingItemEntity TEST_CLOUD_SERVER_BOOKING_ITEM = HsBookingItemEntity.builder() + .project(TEST_PROJECT) + .type(HsBookingItemType.CLOUD_SERVER) + .caption("test cloud server booking item") + .validity(Range.closedInfinite(LocalDate.of(2020, 1, 15))) + .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 44f5327f..0b231bbd 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 @@ -22,6 +22,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.transaction.annotation.Transactional; +import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -220,9 +221,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup void parentAssetAgent_canAddSubAsset() { context.define("superuser-alex@hostsharing.net"); - final var givenParentAsset = givenParentAsset(MANAGED_SERVER, "vm1011"); - - context.define("person-FirbySusan@example.com"); + final var givenParentAsset = givenParentAsset(MANAGED_WEBSPACE, "fir01"); final var location = RestAssured // @formatter:off .given() @@ -232,9 +231,9 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .body(""" { "parentAssetUuid": "%s", - "type": "MANAGED_WEBSPACE", - "identifier": "fir90", - "caption": "some new ManagedWebspace in client's ManagedServer", + "type": "UNIX_USER", + "identifier": "fir01-temp", + "caption": "some new UnixUser in client's ManagedWebspace", "config": {} } """.formatted(givenParentAsset.getUuid())) @@ -246,9 +245,9 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .contentType(ContentType.JSON) .body("", lenientlyEquals(""" { - "type": "MANAGED_WEBSPACE", - "identifier": "fir90", - "caption": "some new ManagedWebspace in client's ManagedServer", + "type": "UNIX_USER", + "identifier": "fir01-temp", + "caption": "some new UnixUser in client's ManagedWebspace", "config": {} } """)) @@ -265,7 +264,9 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup void propertyValidationsArePerformend_whenAddingAsset() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingItem = givenBookingItem("D-1000111 default project", "some PrivateCloud"); + final var givenBookingItem = givenSomeNewBookingItem("D-1000111 default project", + HsBookingItemType.MANAGED_SERVER, + "some PrivateCloud"); RestAssured // @formatter:off .given() @@ -558,10 +559,22 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup }).assertSuccessful().returnedValue(); } - HsBookingItemEntity givenBookingItem(final String projectCaption, final String bookingItemCaption) { - return bookingItemRepo.findByCaption(bookingItemCaption).stream() - .filter(bi -> bi.getRelatedProject().getCaption().contains(projectCaption)) - .findAny().orElseThrow(); + HsBookingItemEntity givenSomeNewBookingItem(final String projectCaption, final HsBookingItemType bookingItemType, final String bookingItemCaption) { + return jpaAttempt.transacted(() -> { + context.define("superuser-alex@hostsharing.net"); + final var project = projectRepo.findByCaption(projectCaption).getFirst(); + final var resources = switch (bookingItemType) { + case MANAGED_SERVER -> Map.ofEntries(entry("CPUs", 1), entry("RAM", 20), entry("SSD", 25), entry("Traffic", 250)); + default -> new HashMap(); + }; + final var newBookingItem = HsBookingItemEntity.builder() + .project(project) + .type(bookingItemType) + .caption(bookingItemCaption) + .resources(resources) + .build(); + return toCleanup(bookingItemRepo.save(newBookingItem)); + }).assertSuccessful().returnedValue(); } HsHostingAssetEntity givenParentAsset(final HsHostingAssetType assetType, final String assetIdentifier) { @@ -574,16 +587,23 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup @SafeVarargs private HsHostingAssetEntity givenSomeTemporaryHostingAsset(final String identifierSuffix, final HsHostingAssetType hostingAssetType, - final Map.Entry... resources) { + final Map.Entry... config) { return jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net"); + final var bookingItemType = switch (hostingAssetType) { + case CLOUD_SERVER -> HsBookingItemType.CLOUD_SERVER; + case MANAGED_SERVER -> HsBookingItemType.MANAGED_SERVER; + case MANAGED_WEBSPACE -> HsBookingItemType.MANAGED_WEBSPACE; + default -> null; + }; + final var newBookingItem = givenSomeNewBookingItem("D-1000111 default project", bookingItemType, "temp ManagedServer"); final var newAsset = HsHostingAssetEntity.builder() .uuid(UUID.randomUUID()) - .bookingItem(givenBookingItem("D-1000111 default project", "some ManagedServer")) + .bookingItem(newBookingItem) .type(hostingAssetType) .identifier("vm" + identifierSuffix) .caption("some test-asset") - .config(Map.ofEntries(resources)) + .config(Map.ofEntries(config)) .build(); return assetRepo.save(newAsset); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java index 890932b4..96728cca 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntityPatcherUnitTest.java @@ -15,7 +15,7 @@ import java.util.Map; import java.util.UUID; import java.util.stream.Stream; -import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_BOOKING_ITEM; +import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_CLOUD_SERVER_BOOKING_ITEM; 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 HsHostingAssetEntityPatcherUnitTest extends PatchUnitTestBase< protected HsHostingAssetEntity newInitialEntity() { final var entity = new HsHostingAssetEntity(); entity.setUuid(INITIAL_BOOKING_ITEM_UUID); - entity.setBookingItem(TEST_BOOKING_ITEM); + entity.setBookingItem(TEST_CLOUD_SERVER_BOOKING_ITEM); entity.getConfig().putAll(KeyValueMap.from(INITIAL_CONFIG)); entity.setCaption(INITIAL_CAPTION); entity.setAlarmContact(givenInitialContact); 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 e45bdb5b..1dd7c0e1 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 @@ -5,13 +5,13 @@ import org.junit.jupiter.api.Test; import java.util.Map; import static java.util.Map.entry; -import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_BOOKING_ITEM; +import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_CLOUD_SERVER_BOOKING_ITEM; import static org.assertj.core.api.Assertions.assertThat; class HsHostingAssetEntityUnitTest { final HsHostingAssetEntity givenParentAsset = HsHostingAssetEntity.builder() - .bookingItem(TEST_BOOKING_ITEM) + .bookingItem(TEST_CLOUD_SERVER_BOOKING_ITEM) .type(HsHostingAssetType.MANAGED_SERVER) .identifier("vm1234") .caption("some managed asset") @@ -21,7 +21,7 @@ class HsHostingAssetEntityUnitTest { entry("HDD-storage", 2048))) .build(); final HsHostingAssetEntity givenWebspace = HsHostingAssetEntity.builder() - .bookingItem(TEST_BOOKING_ITEM) + .bookingItem(TEST_CLOUD_SERVER_BOOKING_ITEM) .type(HsHostingAssetType.MANAGED_WEBSPACE) .parentAsset(givenParentAsset) .identifier("xyz00") @@ -58,7 +58,7 @@ class HsHostingAssetEntityUnitTest { void toStringContainsAllPropertiesAndResourcesSortedByKey() { assertThat(givenWebspace.toString()).isEqualTo( - "HsHostingAssetEntity(MANAGED_WEBSPACE, xyz00, some managed webspace, MANAGED_SERVER:vm1234, D-1234500:test project:test booking item, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })"); + "HsHostingAssetEntity(MANAGED_WEBSPACE, xyz00, some managed webspace, MANAGED_SERVER:vm1234, D-1234500:test project:test cloud server booking item, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })"); assertThat(givenUnixUser.toString()).isEqualTo( "HsHostingAssetEntity(UNIX_USER, xyz00-web, some unix-user, MANAGED_WEBSPACE:xyz00, { HDD-hard-quota: 512, HDD-soft-quota: 256, SSD-hard-quota: 256, SSD-soft-quota: 128 })"); 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 e5bc1605..5ada81b0 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 @@ -90,6 +90,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu result.assertSuccessful(); assertThat(result.returnedValue()).isNotNull().extracting(HsHostingAssetEntity::getUuid).isNotNull(); assertThatAssetIsPersisted(result.returnedValue()); + assertThat(result.returnedValue().isLoaded()).isFalse(); assertThat(assetRepo.count()).isEqualTo(count + 1); } @@ -413,5 +414,6 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu assertThat(actualResult) .extracting(HsHostingAssetEntity::toString) .contains(serverNames); + actualResult.forEach(loadedEntity -> assertThat(loadedEntity.isLoaded()).isTrue()); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java index ee6644e0..fff0fd56 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java @@ -1,12 +1,16 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import org.junit.jupiter.api.Test; import java.util.Map; import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_CLOUD_SERVER_BOOKING_ITEM; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; import static org.assertj.core.api.Assertions.assertThat; class HsCloudServerHostingAssetValidatorUnitTest { @@ -28,7 +32,28 @@ class HsCloudServerHostingAssetValidatorUnitTest { final var result = validator.validate(cloudServerHostingAssetEntity); // then - assertThat(result).containsExactly("'CLOUD_SERVER:vm1234.config.RAM' is not expected but is set to '2000'"); + assertThat(result).containsExactlyInAnyOrder( + "'CLOUD_SERVER:vm1234.bookingItem' must not be null but is null", + "'CLOUD_SERVER:vm1234.config.RAM' is not expected but is set to '2000'"); + } + + @Test + void validatesInvalidIdentifier() { + // given + final var cloudServerHostingAssetEntity = HsHostingAssetEntity.builder() + .type(CLOUD_SERVER) + .identifier("xyz99") + .bookingItem(TEST_CLOUD_SERVER_BOOKING_ITEM) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(cloudServerHostingAssetEntity.getType()); + + + // when + final var result = validator.validate(cloudServerHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'identifier' expected to match '^vm[0-9][0-9][0-9][0-9]$', but is 'xyz99'"); } @Test @@ -39,4 +64,43 @@ class HsCloudServerHostingAssetValidatorUnitTest { // then assertThat(validator.properties()).map(Map::toString).isEmpty(); } + + @Test + void validatesBookingItemType() { + // given + final var mangedServerHostingAssetEntity = HsHostingAssetEntity.builder() + .type(MANAGED_SERVER) + .identifier("xyz00") + .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build()) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); + + // when + final var result = validator.validate(mangedServerHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'MANAGED_SERVER:xyz00.bookingItem' must be of type MANAGED_SERVER but is of type CLOUD_SERVER"); + } + + @Test + void validatesParentAndAssignedToAssetMustNotBeSet() { + // given + final var mangedServerHostingAssetEntity = HsHostingAssetEntity.builder() + .type(CLOUD_SERVER) + .identifier("xyz00") + .parentAsset(HsHostingAssetEntity.builder().build()) + .assignedToAsset(HsHostingAssetEntity.builder().build()) + .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build()) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); + + // when + final var result = validator.validate(mangedServerHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'CLOUD_SERVER:xyz00.parentAsset' must be null but is set to D-???????-?:null", + "'CLOUD_SERVER:xyz00.assignedToAsset' must be null but is set to D-???????-?:null"); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java new file mode 100644 index 00000000..32c098f3 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java @@ -0,0 +1,64 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.assertj.core.api.Assertions.entry; + +class HsHostingAssetEntityValidatorRegistryUnitTest { + + @Test + void forTypeWithUnknownTypeThrowsException() { + // when + final var thrown = catchThrowable(() -> { + HsHostingAssetEntityValidatorRegistry.forType(null); + }); + + // then + assertThat(thrown).hasMessage("no validator found for type null"); + } + + @Test + void typesReturnsAllImplementedTypes() { + // when + final var types = HsHostingAssetEntityValidatorRegistry.types(); + + // then + // TODO.test: when all types are implemented, replace with set of all types: + // assertThat(types).isEqualTo(EnumSet.allOf(HsHostingAssetType.class)); + // also remove "Implemented" from the test method name. + assertThat(types).containsExactlyInAnyOrder( + HsHostingAssetType.CLOUD_SERVER, + HsHostingAssetType.MANAGED_SERVER, + HsHostingAssetType.MANAGED_WEBSPACE, + HsHostingAssetType.UNIX_USER + ); + } + + @Test + void validatedDoesNotThrowAnExceptionForValidEntity() { + final var givenBookingItem = HsBookingItemEntity.builder() + .type(HsBookingItemType.CLOUD_SERVER) + .resources(Map.ofEntries( + entry("CPUs", 4), + entry("RAM", 20), + entry("SSD", 50), + entry("Traffic", 250) + )) + .build(); + final var validEntity = HsHostingAssetEntity.builder() + .type(HsHostingAssetType.CLOUD_SERVER) + .bookingItem(givenBookingItem) + .identifier("vm1234") + .caption("some valid cloud server") + .build(); + HsHostingAssetEntityValidatorRegistry.validated(validEntity); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java index ddceba8e..73776e89 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java @@ -1,5 +1,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import org.junit.jupiter.api.Test; @@ -16,12 +18,18 @@ class HsHostingAssetEntityValidatorUnitTest { final var managedServerHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_SERVER) .identifier("vm1234") + .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.MANAGED_SERVER).build()) + .parentAsset(HsHostingAssetEntity.builder().type(MANAGED_SERVER).build()) + .assignedToAsset(HsHostingAssetEntity.builder().type(MANAGED_SERVER).build()) .build(); // when final var result = catchThrowable( ()-> HsHostingAssetEntityValidatorRegistry.validated(managedServerHostingAssetEntity)); // then - assertThat(result).isNull(); // all required properties have defaults + assertThat(result.getMessage()).contains( + "'MANAGED_SERVER:vm1234.parentAsset' must be null but is set to D-???????-?:null", + "'MANAGED_SERVER:vm1234.assignedToAsset' must be null but is set to D-???????-?:null" + ); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java index d22ef590..010bbf54 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java @@ -1,5 +1,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import org.junit.jupiter.api.Test; @@ -17,6 +19,9 @@ class HsManagedServerHostingAssetValidatorUnitTest { final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_SERVER) .identifier("vm1234") + .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.MANAGED_SERVER).build()) + .parentAsset(HsHostingAssetEntity.builder().build()) + .assignedToAsset(HsHostingAssetEntity.builder().build()) .config(Map.ofEntries( entry("monit_max_hdd_usage", "90"), entry("monit_max_cpu_usage", 2), @@ -30,8 +35,50 @@ class HsManagedServerHostingAssetValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( + "'MANAGED_SERVER:vm1234.parentAsset' must be null but is set to D-???????-?:null", + "'MANAGED_SERVER:vm1234.assignedToAsset' must be null but is set to D-???????-?:null", "'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be >= 10 but is 2", "'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be <= 100 but is 101", "'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'"); } + + @Test + void validatesInvalidIdentifier() { + // given + final var mangedServerHostingAssetEntity = HsHostingAssetEntity.builder() + .type(MANAGED_SERVER) + .identifier("xyz00") + .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.MANAGED_SERVER).build()) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); + + // when + final var result = validator.validate(mangedServerHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'identifier' expected to match '^vm[0-9][0-9][0-9][0-9]$', but is 'xyz00'"); + } + + @Test + void validatesParentAndAssignedToAssetMustNotBeSet() { + // given + final var mangedServerHostingAssetEntity = HsHostingAssetEntity.builder() + .type(MANAGED_SERVER) + .identifier("xyz00") + .parentAsset(HsHostingAssetEntity.builder().build()) + .assignedToAsset(HsHostingAssetEntity.builder().build()) + .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build()) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); + + // when + final var result = validator.validate(mangedServerHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'MANAGED_SERVER:xyz00.bookingItem' must be of type MANAGED_SERVER but is of type CLOUD_SERVER", + "'MANAGED_SERVER:xyz00.parentAsset' must be null but is set to D-???????-?:null", + "'MANAGED_SERVER:xyz00.assignedToAsset' must be null but is set to D-???????-?:null"); + } } 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 d2e74894..7b981b68 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 @@ -28,6 +28,11 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { entry("SLA-EMail", true) )) .build(); + final HsBookingItemEntity cloudServerBookingItem = managedServerBookingItem.toBuilder() + .type(HsBookingItemType.CLOUD_SERVER) + .caption("Test Cloud-Server") + .build(); + final HsHostingAssetEntity mangedServerAssetEntity = HsHostingAssetEntity.builder() .type(HsHostingAssetType.MANAGED_SERVER) .bookingItem(managedServerBookingItem) @@ -38,13 +43,46 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { entry("monit_max_ram_usage", 90) )) .build(); + final HsHostingAssetEntity cloudServerAssetEntity = HsHostingAssetEntity.builder() + .type(HsHostingAssetType.CLOUD_SERVER) + .bookingItem(cloudServerBookingItem) + .identifier("vm1234") + .config(Map.ofEntries( + entry("monit_max_ssd_usage", 70), + entry("monit_max_cpu_usage", 80), + entry("monit_max_ram_usage", 90) + )) + .build(); @Test - void validatesIdentifier() { + void acceptsAlienIdentifierPrefixForPreExistingEntity() { // given final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE); final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_WEBSPACE) + .bookingItem(HsBookingItemEntity.builder() + .type(HsBookingItemType.MANAGED_WEBSPACE) + .resources(Map.ofEntries(entry("SSD", 25), entry("Traffic", 250))) + .build()) + .parentAsset(mangedServerAssetEntity) + .identifier("xyz00") + .isLoaded(true) + .build(); + + // when + final var result = validator.validate(mangedWebspaceHostingAssetEntity); + + // then + assertThat(result).isEmpty(); + } + + @Test + void validatesIdentifierAndReferencedEntities() { + // given + final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE); + final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() + .type(MANAGED_WEBSPACE) + .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.MANAGED_WEBSPACE).build()) .parentAsset(mangedServerAssetEntity) .identifier("xyz00") .build(); @@ -62,6 +100,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE); final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_WEBSPACE) + .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.MANAGED_WEBSPACE).build()) .parentAsset(mangedServerAssetEntity) .identifier("abc00") .config(Map.ofEntries( @@ -82,6 +121,11 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE); final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_WEBSPACE) + .bookingItem(HsBookingItemEntity.builder() + .type(HsBookingItemType.MANAGED_WEBSPACE) + .caption("some ManagedWebspace") + .resources(Map.ofEntries(entry("SSD", 25), entry("Traffic", 250))) + .build()) .parentAsset(mangedServerAssetEntity) .identifier("abc00") .build(); @@ -92,4 +136,30 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { // then assertThat(result).isEmpty(); } + + @Test + void validatesEntityReferences() { + // given + final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE); + final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() + .type(MANAGED_WEBSPACE) + .bookingItem(HsBookingItemEntity.builder() + .type(HsBookingItemType.MANAGED_SERVER) + .caption("some ManagedServer") + .resources(Map.ofEntries(entry("SSD", 25), entry("Traffic", 250))) + .build()) + .parentAsset(cloudServerAssetEntity) + .assignedToAsset(HsHostingAssetEntity.builder().build()) + .identifier("abc00") + .build(); + + // when + final var result = validator.validate(mangedWebspaceHostingAssetEntity); + + // then + assertThat(result).containsExactly( + "'MANAGED_WEBSPACE:abc00.bookingItem' must be of type MANAGED_WEBSPACE but is of type MANAGED_SERVER", + "'MANAGED_WEBSPACE:abc00.parentAsset' must be of type MANAGED_SERVER but is of type CLOUD_SERVER", + "'MANAGED_WEBSPACE:abc00.assignedToAsset' must be null but is set to D-???????-?:some ManagedServer"); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java new file mode 100644 index 00000000..afe265b0 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java @@ -0,0 +1,30 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import org.junit.jupiter.api.Test; + +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER; +import static org.assertj.core.api.Assertions.assertThat; + +class HsUnixUserHostingAssetValidatorUnitTest { + + @Test + void validatesInvalidIdentifier() { + // given + final var unixUserHostingAsset = HsHostingAssetEntity.builder() + .type(UNIX_USER) + .parentAsset(HsHostingAssetEntity.builder().type(MANAGED_WEBSPACE).identifier("abc00").build()) + .identifier("xyz99-temp") + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); + + + // when + final var result = validator.validate(unixUserHostingAsset); + + // then + assertThat(result).containsExactly( + "'identifier' expected to match '^abc00$|^abc00-[a-z0-9]+$', but is 'xyz99-temp'"); + } +} From 6167ef22216e6c160d680a0b319a4d2329ce354f Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Thu, 27 Jun 2024 12:39:44 +0200 Subject: [PATCH 10/18] add-unix-user-hosting-asset-validation (#66) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/66 Reviewed-by: Marc Sandlus --- .../hs/booking/item/HsBookingItemEntity.java | 21 +- .../HsBookingItemEntityValidator.java | 2 +- .../asset/HsHostingAssetController.java | 19 +- .../hosting/asset/HsHostingAssetEntity.java | 28 ++- .../HsHostingAssetEntityValidator.java | 6 +- ...HsHostingAssetEntityValidatorRegistry.java | 18 +- .../HsUnixUserHostingAssetValidator.java | 35 ++- .../hs/validation/BooleanProperty.java | 6 +- .../hs/validation/EnumerationProperty.java | 15 +- .../hs/validation/HsEntityValidator.java | 27 ++- .../hs/validation/IntegerProperty.java | 52 ++++- .../hs/validation/PasswordProperty.java | 65 ++++++ .../hs/validation/PropertiesProvider.java | 31 +++ .../hs/validation/StringProperty.java | 79 +++++++ .../hs/validation/ValidatableProperty.java | 89 +++++-- .../hostsharing/hsadminng/mapper/Array.java | 5 + .../hsadminng/mapper/PatchableMapWrapper.java | 15 +- .../item/HsBookingItemEntityUnitTest.java | 2 +- ...sBookingItemRepositoryIntegrationTest.java | 20 +- .../hs/booking/item/TestHsBookingItem.java | 36 ++- ...sHostingAssetControllerAcceptanceTest.java | 221 +++++++++++++----- .../asset/HsHostingAssetEntityUnitTest.java | 12 +- ...ingAssetPropsControllerAcceptanceTest.java | 108 ++------- ...HostingAssetRepositoryIntegrationTest.java | 4 +- ...edServerHostingAssetValidatorUnitTest.java | 4 +- ...UnixUserHostingAssetValidatorUnitTest.java | 103 +++++++- .../validation/PasswordPropertyUnitTest.java | 92 ++++++++ .../hsadminng/rbac/test/JsonMatcher.java | 13 +- 28 files changed, 895 insertions(+), 233 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/validation/PropertiesProvider.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java 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 94b80984..ba1d2a7e 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 @@ -11,6 +11,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.validation.PropertiesProvider; import net.hostsharing.hsadminng.mapper.PatchableMapWrapper; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; @@ -42,6 +43,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import static java.util.Collections.emptyMap; import static java.util.Optional.ofNullable; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveFromPostgresDateRange; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange; @@ -68,7 +70,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @Setter @NoArgsConstructor @AllArgsConstructor -public class HsBookingItemEntity implements Stringifyable, RbacObject { +public class HsBookingItemEntity implements Stringifyable, RbacObject, PropertiesProvider { private static Stringify stringify = stringify(HsBookingItemEntity.class) .withProp(HsBookingItemEntity::getProject) @@ -146,6 +148,23 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject { return upperInclusiveFromPostgresDateRange(getValidity()); } + @Override + public Map directProps() { + return resources; + } + + @Override + public Object getContextValue(final String propName) { + final var v = resources.get(propName); + if (v!= null) { + return v; + } + if (parentItem!=null) { + return parentItem.getResources().get(propName); + } + return emptyMap(); + } + @Override public String toString() { return stringify.apply(this); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java index ee07e981..315de471 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java @@ -29,7 +29,7 @@ public class HsBookingItemEntityValidator extends HsEntityValidator validateProperties(final HsBookingItemEntity bookingItem) { - return enrich(prefix(bookingItem.toShortString(), "resources"), validateProperties(bookingItem.getResources())); + return enrich(prefix(bookingItem.toShortString(), "resources"), super.validateProperties(bookingItem)); } private static List optionallyValidate(final HsBookingItemEntity bookingItem) { 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 b7982328..b0e5cd62 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 @@ -1,6 +1,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; +import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetsApi; import net.hostsharing.hsadminng.context.Context; @@ -78,7 +79,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { .path("/api/hs/hosting/assets/{id}") .buildAndExpand(saved.getUuid()) .toUri(); - final var mapped = mapper.map(saved, HsHostingAssetResource.class); + final var mapped = mapper.map(saved, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.created(uri).body(mapped); } @@ -94,7 +95,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { final var result = assetRepo.findByUuid(assetUuid); return result .map(assetEntity -> ResponseEntity.ok( - mapper.map(assetEntity, HsHostingAssetResource.class))) + mapper.map(assetEntity, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER))) .orElseGet(() -> ResponseEntity.notFound().build()); } @@ -126,8 +127,17 @@ public class HsHostingAssetController implements HsHostingAssetsApi { new HsHostingAssetEntityPatcher(em, current).apply(body); +// TODO.refa: draft for an alternative API +// validate(current) // self-validation, hashing passwords etc. +// .then(HsHostingAssetEntityValidatorRegistry::prepareForSave) // hashing passwords etc. +// .then(assetRepo::save) +// .then(HsHostingAssetEntityValidatorRegistry::validateInContext) +// // In this last step we need the entity and the mapped resource instance, +// // which is exactly what a postmapper takes as arguments. +// .then(this::mapToResource) using postProcessProperties to remove write-only + add read-only properties + final var saved = validated(assetRepo.save(current)); - final var mapped = mapper.map(saved, HsHostingAssetResource.class); + final var mapped = mapper.map(saved, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(mapped); } @@ -144,4 +154,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { resource.getParentAssetUuid())))); } }; + + final BiConsumer ENTITY_TO_RESOURCE_POSTMAPPER + = HsHostingAssetEntityValidatorRegistry::postprocessProperties; } 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 ff7bfd33..ae181921 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 @@ -9,6 +9,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; +import net.hostsharing.hsadminng.hs.validation.PropertiesProvider; import net.hostsharing.hsadminng.mapper.PatchableMapWrapper; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; @@ -39,6 +40,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import static java.util.Collections.emptyMap; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL; @@ -63,7 +65,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @Setter @NoArgsConstructor @AllArgsConstructor -public class HsHostingAssetEntity implements Stringifyable, RbacObject { +public class HsHostingAssetEntity implements Stringifyable, RbacObject, PropertiesProvider { private static Stringify stringify = stringify(HsHostingAssetEntity.class) .withProp(HsHostingAssetEntity::getType) @@ -122,7 +124,7 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject { private PatchableMapWrapper configWrapper; @Transient - private boolean isLoaded = false; + private boolean isLoaded; @PostLoad public void markAsLoaded() { @@ -137,6 +139,28 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject { PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfig); } + @Override + public Map directProps() { + return config; + } + + @Override + public Object getContextValue(final String propName) { + final var v = config.get(propName); + if (v!= null) { + return v; + } + + if (bookingItem!=null) { + return bookingItem.getResources().get(propName); + } + if (parentAsset!=null && parentAsset.getBookingItem()!=null) { + return parentAsset.getBookingItem().getResources().get(propName); + } + return emptyMap(); + } + + @Override public String toString() { return stringify.apply(this); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java index 15ea12df..05bcee97 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java @@ -47,7 +47,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator validate(final HsHostingAssetEntity assetEntity) { return sequentiallyValidate( - () -> validateEntityReferences(assetEntity), + () -> validateEntityReferencesAndProperties(assetEntity), () -> validateIdentifierPattern(assetEntity), // might need proper parentAsset or billingItem () -> optionallyValidate(assetEntity.getBookingItem()), () -> optionallyValidate(assetEntity.getParentAsset()), @@ -55,7 +55,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator validateEntityReferences(final HsHostingAssetEntity assetEntity) { + private List validateEntityReferencesAndProperties(final HsHostingAssetEntity assetEntity) { return Stream.of( validateReferencedEntity(assetEntity, "bookingItem", bookingItemValidation::validate), validateReferencedEntity(assetEntity, "parentAsset", parentAssetValidation::validate), @@ -76,7 +76,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator validateProperties(final HsHostingAssetEntity assetEntity) { - return enrich(prefix(assetEntity.toShortString(), "config"), validateProperties(assetEntity.getConfig())); + return enrich(prefix(assetEntity.toShortString(), "config"), super.validateProperties(assetEntity)); } private static List optionallyValidate(final HsHostingAssetEntity assetEntity) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java index 1b9a5241..a5331f81 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java @@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; +import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource; import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; import net.hostsharing.hsadminng.errors.MultiValidationException; @@ -40,7 +41,8 @@ public class HsHostingAssetEntityValidatorRegistry { } public static List doValidate(final HsHostingAssetEntity hostingAsset) { - return HsHostingAssetEntityValidatorRegistry.forType(hostingAsset.getType()).validate(hostingAsset); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(hostingAsset.getType()); + return validator.validate(hostingAsset); } public static HsHostingAssetEntity validated(final HsHostingAssetEntity entityToSave) { @@ -48,4 +50,18 @@ public class HsHostingAssetEntityValidatorRegistry { return entityToSave; } + public static void postprocessProperties(final HsHostingAssetEntity entity, final HsHostingAssetResource resource) { + final var validator = HsHostingAssetEntityValidatorRegistry.forType(entity.getType()); + final var config = validator.postProcess(entity, asMap(resource)); + resource.setConfig(config); + } + + @SuppressWarnings("unchecked") + private static Map asMap(final HsHostingAssetResource resource) { + if (resource.getConfig() instanceof Map map) { + return map; + } + throw new IllegalArgumentException("expected a Map, but got a " + resource.getConfig().getClass()); + } + } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java index dfe222fc..74e59965 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java @@ -2,17 +2,35 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; +import net.hostsharing.hsadminng.hs.validation.PropertiesProvider; import java.util.regex.Pattern; +import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty; +import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; +import static net.hostsharing.hsadminng.hs.validation.PasswordProperty.passwordProperty; +import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; + class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator { + private static final int DASH_LENGTH = "-".length(); + HsUnixUserHostingAssetValidator() { - super(BookingItem.mustBeNull(), - ParentAsset.mustBeOfType(HsHostingAssetType.MANAGED_WEBSPACE), - AssignedToAsset.mustBeNull(), - AlarmContact.isOptional(), // TODO.spec: for quota notifications - NO_EXTRA_PROPERTIES); // TODO.spec: yet to be specified + super( BookingItem.mustBeNull(), + ParentAsset.mustBeOfType(HsHostingAssetType.MANAGED_WEBSPACE), + AssignedToAsset.mustBeNull(), + AlarmContact.isOptional(), + + integerProperty("SSD hard quota").unit("GB").maxFrom("SSD").optional(), + integerProperty("SSD soft quota").unit("GB").maxFrom("SSD hard quota").optional(), + integerProperty("HDD hard quota").unit("GB").maxFrom("HDD").optional(), + integerProperty("HDD soft quota").unit("GB").maxFrom("HDD hard quota").optional(), + enumerationProperty("shell") + .values("/bin/false", "/bin/bash", "/bin/csh", "/bin/dash", "/usr/bin/tcsh", "/usr/bin/zsh", "/usr/bin/passwd") + .withDefault("/bin/false"), + stringProperty("homedir").readOnly().computedBy(HsUnixUserHostingAssetValidator::computeHomedir), + stringProperty("totpKey").matchesRegEx("^0x([0-9A-Fa-f]{2})+$").minLength(20).maxLength(256).undisclosed().writeOnly().optional(), + passwordProperty("password").minLength(8).maxLength(40).writeOnly()); } @Override @@ -20,4 +38,11 @@ class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator { final var webspaceIdentifier = assetEntity.getParentAsset().getIdentifier(); return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"-[a-z0-9]+$"); } + + private static String computeHomedir(final PropertiesProvider propertiesProvider) { + final var entity = (HsHostingAssetEntity) propertiesProvider; + final var webspaceName = entity.getParentAsset().getIdentifier(); + return "/home/pacs/" + webspaceName + + "/users/" + entity.getIdentifier().substring(webspaceName.length()+DASH_LENGTH); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java index 9d664683..5f893d74 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java @@ -4,7 +4,7 @@ import lombok.Setter; import net.hostsharing.hsadminng.mapper.Array; import java.util.AbstractMap; -import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -29,9 +29,9 @@ public class BooleanProperty extends ValidatableProperty { } @Override - protected void validate(final ArrayList result, final Boolean propValue, final Map props) { + protected void validate(final List result, final Boolean propValue, final PropertiesProvider propProvider) { if (falseIf != null && propValue) { - final Object referencedValue = props.get(falseIf.getKey()); + final Object referencedValue = propProvider.directProps().get(falseIf.getKey()); if (Objects.equals(referencedValue, falseIf.getValue())) { result.add(propertyName + "' is expected to be false because " + falseIf.getKey() + "=" + referencedValue + " but is " + propValue); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java index 923d7ae1..60af1b73 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java @@ -3,9 +3,8 @@ package net.hostsharing.hsadminng.hs.validation; import lombok.Setter; import net.hostsharing.hsadminng.mapper.Array; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Map; +import java.util.List; import static java.util.Arrays.stream; @@ -33,25 +32,25 @@ public class EnumerationProperty extends ValidatableProperty { } public void deferredInit(final ValidatableProperty[] allProperties) { - if (deferredInit != null) { + if (hasDeferredInit()) { if (this.values != null) { - throw new IllegalStateException("property " + toString() + " already has values"); + throw new IllegalStateException("property " + this + " already has values"); } - this.values = deferredInit.apply(allProperties); + this.values = doDeferredInit(allProperties); } } public ValidatableProperty valuesFromProperties(final String propertyNamePrefix) { - this.deferredInit = (ValidatableProperty[] allProperties) -> stream(allProperties) + this.setDeferredInit( (ValidatableProperty[] allProperties) -> stream(allProperties) .map(ValidatableProperty::propertyName) .filter(name -> name.startsWith(propertyNamePrefix)) .map(name -> name.substring(propertyNamePrefix.length())) - .toArray(String[]::new); + .toArray(String[]::new)); return this; } @Override - protected void validate(final ArrayList result, final String propValue, final Map props) { + protected void validate(final List result, final String propValue, final PropertiesProvider propProvider) { if (stream(values).noneMatch(v -> v.equals(propValue))) { result.add(propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'"); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java index 4c20f2a5..5af7118d 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.validation; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -10,7 +11,8 @@ import java.util.function.Supplier; import static java.util.Arrays.stream; import static java.util.Collections.emptyList; -public abstract class HsEntityValidator { +// TODO.refa: rename to HsEntityProcessor, also subclasses +public abstract class HsEntityValidator { public final ValidatableProperty[] propertyValidators; @@ -38,16 +40,22 @@ public abstract class HsEntityValidator { .toList(); } - protected ArrayList validateProperties(final Map properties) { + protected ArrayList validateProperties(final PropertiesProvider propsProvider) { final var result = new ArrayList(); + + // verify that all actually given properties are specified + final var properties = propsProvider.directProps(); properties.keySet().forEach( givenPropName -> { if (stream(propertyValidators).map(pv -> pv.propertyName).noneMatch(propName -> propName.equals(givenPropName))) { result.add(givenPropName + "' is not expected but is set to '" + properties.get(givenPropName) + "'"); } }); + + // run all property validators stream(propertyValidators).forEach(pv -> { - result.addAll(pv.validate(properties)); + result.addAll(pv.validate(propsProvider)); }); + return result; } @@ -80,4 +88,17 @@ public abstract class HsEntityValidator { } throw new IllegalArgumentException("Integer value (or null) expected, but got " + value); } + + public Map postProcess(final E entity, final Map config) { + final var copy = new HashMap<>(config); + stream(propertyValidators).forEach(p -> { + if ( p.isWriteOnly()) { + copy.remove(p.propertyName); + } + if (p.isComputed()) { + copy.put(p.propertyName, p.compute(entity)); + } + }); + return copy; + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java index a1658ff9..f185c469 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java @@ -2,21 +2,23 @@ package net.hostsharing.hsadminng.hs.validation; import lombok.Setter; import net.hostsharing.hsadminng.mapper.Array; +import org.apache.commons.lang3.Validate; -import java.util.ArrayList; -import java.util.Map; +import java.util.List; @Setter public class IntegerProperty extends ValidatableProperty { private final static String[] KEY_ORDER = Array.join( ValidatableProperty.KEY_ORDER_HEAD, - Array.of("unit", "min", "max", "step"), + Array.of("unit", "min", "minFrom", "max", "maxFrom", "step"), ValidatableProperty.KEY_ORDER_TAIL); private String unit; private Integer min; + private String minFrom; private Integer max; + private String maxFrom; private Integer step; public static IntegerProperty integerProperty(final String propertyName) { @@ -27,6 +29,22 @@ public class IntegerProperty extends ValidatableProperty { super(Integer.class, propertyName, KEY_ORDER); } + @Override + public void deferredInit(final ValidatableProperty[] allProperties) { + Validate.isTrue(min == null || minFrom == null, "min and minFrom are exclusive, but both are given"); + Validate.isTrue(max == null || maxFrom == null, "max and maxFrom are exclusive, but both are given"); + } + + public IntegerProperty minFrom(final String propertyName) { + minFrom = propertyName; + return this; + } + + public IntegerProperty maxFrom(final String propertyName) { + maxFrom = propertyName; + return this; + } + @Override public String unit() { return unit; @@ -37,20 +55,34 @@ public class IntegerProperty extends ValidatableProperty { } @Override - protected void validate(final ArrayList result, final Integer propValue, final Map props) { - if (min != null && propValue < min) { - result.add(propertyName + "' is expected to be >= " + min + " but is " + propValue); - } - if (max != null && propValue > max) { - result.add(propertyName + "' is expected to be <= " + max + " but is " + propValue); - } + protected void validate(final List result, final Integer propValue, final PropertiesProvider propProvider) { + validateMin(result, propertyName, propValue, min); + validateMax(result, propertyName, propValue, max); if (step != null && propValue % step != 0) { result.add(propertyName + "' is expected to be multiple of " + step + " but is " + propValue); } + if (minFrom != null) { + validateMin(result, propertyName, propValue, propProvider.getContextValue(minFrom, Integer.class)); + } + if (maxFrom != null) { + validateMax(result, propertyName, propValue, propProvider.getContextValue(maxFrom, Integer.class, 0)); + } } @Override protected String simpleTypeName() { return "integer"; } + + private static void validateMin(final List result, final String propertyName, final Integer propValue, final Integer min) { + if (min != null && propValue < min) { + result.add(propertyName + "' is expected to be at least " + min + " but is " + propValue); + } + } + + private static void validateMax(final List result, final String propertyName, final Integer propValue, final Integer max) { + if (max != null && propValue > max) { + result.add(propertyName + "' is expected to be at most " + max + " but is " + propValue); + } + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java new file mode 100644 index 00000000..92cafb9a --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java @@ -0,0 +1,65 @@ +package net.hostsharing.hsadminng.hs.validation; + +import lombok.Setter; + +import java.util.List; +import java.util.stream.Stream; + +@Setter +public class PasswordProperty extends StringProperty { + + private PasswordProperty(final String propertyName) { + super(propertyName); + undisclosed(); + } + + public static PasswordProperty passwordProperty(final String propertyName) { + return new PasswordProperty(propertyName); + } + + @Override + protected void validate(final List result, final String propValue, final PropertiesProvider propProvider) { + super.validate(result, propValue, propProvider); + validatePassword(result, propValue); + } + + // TODO.impl: only a SHA512 hash should be stored in the database, not the password itself + + @Override + protected String simpleTypeName() { + return "password"; + } + + private void validatePassword(final List result, final String password) { + boolean hasLowerCase = false; + boolean hasUpperCase = false; + boolean hasDigit = false; + boolean hasSpecialChar = false; + boolean containsColon = false; + + for (char c : password.toCharArray()) { + if (Character.isLowerCase(c)) { + hasLowerCase = true; + } else if (Character.isUpperCase(c)) { + hasUpperCase = true; + } else if (Character.isDigit(c)) { + hasDigit = true; + } else if (!Character.isLetterOrDigit(c)) { + hasSpecialChar = true; + } + + if (c == ':') { + containsColon = true; + } + } + + final long groupsCovered = Stream.of(hasLowerCase, hasUpperCase, hasDigit, hasSpecialChar).filter(v->v).count(); + if ( groupsCovered < 3) { + result.add(propertyName + "' must contain at least one character of at least 3 of the following groups: upper case letters, lower case letters, digits, special characters"); + } + if (containsColon) { + result.add(propertyName + "' must not contain colon (':')"); + } + + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/PropertiesProvider.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/PropertiesProvider.java new file mode 100644 index 00000000..c4d60fb8 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/PropertiesProvider.java @@ -0,0 +1,31 @@ +package net.hostsharing.hsadminng.hs.validation; + +import java.util.Map; + +public interface PropertiesProvider { + + Map directProps(); + Object getContextValue(final String propName); + + default T getDirectValue(final String propName, final Class clazz) { + return cast(propName, directProps().get(propName), clazz, null); + } + + default T getContextValue(final String propName, final Class clazz) { + return cast(propName, getContextValue(propName), clazz, null); + } + + default T getContextValue(final String propName, final Class clazz, final T defaultValue) { + return cast(propName, getContextValue(propName), clazz, defaultValue); + } + + private static T cast( final String propName, final Object value, final Class clazz, final T defaultValue) { + if (value == null && defaultValue != null) { + return defaultValue; + } + if (value == null || clazz.isInstance(value)) { + return clazz.cast(value); + } + throw new IllegalStateException(propName + " expected to be an "+clazz.getSimpleName()+", but got '" + value + "'"); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java new file mode 100644 index 00000000..a499d951 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java @@ -0,0 +1,79 @@ +package net.hostsharing.hsadminng.hs.validation; + +import lombok.Setter; +import net.hostsharing.hsadminng.mapper.Array; + +import java.util.List; +import java.util.regex.Pattern; + +@Setter +public class StringProperty extends ValidatableProperty { + + private static final String[] KEY_ORDER = Array.join( + ValidatableProperty.KEY_ORDER_HEAD, + Array.of("matchesRegEx", "minLength", "maxLength"), + ValidatableProperty.KEY_ORDER_TAIL, + Array.of("undisclosed")); + private Pattern matchesRegEx; + private Integer minLength; + private Integer maxLength; + private boolean undisclosed; + + protected StringProperty(final String propertyName) { + super(String.class, propertyName, KEY_ORDER); + } + + public static StringProperty stringProperty(final String propertyName) { + return new StringProperty(propertyName); + } + + public StringProperty minLength(final int minLength) { + this.minLength = minLength; + return this; + } + + public StringProperty maxLength(final int maxLength) { + this.maxLength = maxLength; + return this; + } + + public StringProperty matchesRegEx(final String regExPattern) { + this.matchesRegEx = Pattern.compile(regExPattern); + return this; + } + + /** + * The property value is not disclosed in error messages. + * + * @return this; + */ + public StringProperty undisclosed() { + this.undisclosed = true; + return this; + } + + @Override + protected void validate(final List result, final String propValue, final PropertiesProvider propProvider) { + if (minLength != null && propValue.length()maxLength) { + result.add(propertyName + "' length is expected to be at max " + maxLength + " but length of " + display(propValue) + " is " + propValue.length()); + } + if (matchesRegEx != null && !matchesRegEx.matcher(propValue).matches()) { + result.add(propertyName + "' is expected to be match " + matchesRegEx + " but " + display(propValue) + " does not match"); + } + if (isReadOnly() && propValue != null) { + result.add(propertyName + "' is readonly but given as " + display(propValue)); + } + } + + private String display(final String propValue) { + return undisclosed ? "provided value" : ("'" + propValue + "'"); + } + + @Override + protected String simpleTypeName() { + return "string"; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java index 3b0bb099..b34eb8fa 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java @@ -1,6 +1,8 @@ package net.hostsharing.hsadminng.hs.validation; import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.experimental.Accessors; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; @@ -21,19 +23,37 @@ import static java.lang.Boolean.TRUE; import static java.util.Collections.emptyList; import static java.util.Optional.ofNullable; +@Getter @RequiredArgsConstructor public abstract class ValidatableProperty { protected static final String[] KEY_ORDER_HEAD = Array.of("propertyName"); - protected static final String[] KEY_ORDER_TAIL = Array.of("required", "defaultValue", "isTotalsValidator", "thresholdPercentage"); + protected static final String[] KEY_ORDER_TAIL = Array.of("required", "defaultValue", "readOnly", "writeOnly", "computed", "isTotalsValidator", "thresholdPercentage"); final Class type; final String propertyName; + + @JsonIgnore private final String[] keyOrder; + private Boolean required; private T defaultValue; - protected Function[], T[]> deferredInit; + + @JsonIgnore + private Function computedBy; + + @Accessors(makeFinal = true, chain = true, fluent = false) + private boolean computed; // used in descriptor, because computedBy cannot be rendered to a text string + + @Accessors(makeFinal = true, chain = true, fluent = false) + private boolean readOnly; + + @Accessors(makeFinal = true, chain = true, fluent = false) + private boolean writeOnly; + + private Function[], T[]> deferredInit; private boolean isTotalsValidator = false; + @JsonIgnore private List>> asTotalLimitValidators; // TODO.impl: move to BookingItemIntegerProperty @@ -43,6 +63,30 @@ public abstract class ValidatableProperty { return null; } + protected void setDeferredInit(final Function[], T[]> function) { + this.deferredInit = function; + } + + public boolean hasDeferredInit() { + return deferredInit != null; + } + + public T[] doDeferredInit(final ValidatableProperty[] allProperties) { + return deferredInit.apply(allProperties); + } + + public ValidatableProperty writeOnly() { + this.writeOnly = true; + optional(); + return this; + } + + public ValidatableProperty readOnly() { + this.readOnly = true; + optional(); + return this; + } + public ValidatableProperty required() { required = TRUE; return this; @@ -116,8 +160,9 @@ public abstract class ValidatableProperty { return this; } - public final List validate(final Map props) { + public final List validate(final PropertiesProvider propsProvider) { final var result = new ArrayList(); + final var props = propsProvider.directProps(); final var propValue = props.get(propertyName); if (propValue == null) { if (required) { @@ -127,7 +172,7 @@ public abstract class ValidatableProperty { if (propValue != null){ if ( type.isInstance(propValue)) { //noinspection unchecked - validate(result, (T) propValue, props); + validate(result, (T) propValue, propsProvider); } else { result.add(propertyName + "' is expected to be of type " + type + ", " + "but is of type '" + propValue.getClass().getSimpleName() + "'"); @@ -136,7 +181,7 @@ public abstract class ValidatableProperty { return result; } - protected abstract void validate(final ArrayList result, final T propValue, final Map props); + protected abstract void validate(final List result, final T propValue, final PropertiesProvider propProvider); public void verifyConsistency(final Map.Entry, ?> typeDef) { if (required == null ) { @@ -158,26 +203,32 @@ public abstract class ValidatableProperty { // Add entries according to the given order for (String key : keyOrder) { final Optional propValue = getPropertyValue(key); - propValue.ifPresent(o -> sortedMap.put(key, o)); + propValue.filter(ValidatableProperty::isToBeRendered).ifPresent(o -> sortedMap.put(key, o)); } return sortedMap; } + private static boolean isToBeRendered(final Object v) { + return !(v instanceof Boolean b) || b; + } + @SneakyThrows private Optional getPropertyValue(final String key) { + return getPropertyValue(getClass(), key); + } + + @SneakyThrows + private Optional getPropertyValue(final Class clazz, final String key) { try { - final var field = getClass().getDeclaredField(key); + final var field = clazz.getDeclaredField(key); field.setAccessible(true); return Optional.ofNullable(arrayToList(field.get(this))); - } catch (final NoSuchFieldException e1) { - try { - final var field = getClass().getSuperclass().getDeclaredField(key); - field.setAccessible(true); - return Optional.ofNullable(arrayToList(field.get(this))); - } catch (final NoSuchFieldException e2) { - return Optional.empty(); + } catch (final NoSuchFieldException exc) { + if (clazz.getSuperclass() != null) { + return getPropertyValue(clazz.getSuperclass(), key); } + throw exc; } } @@ -198,4 +249,14 @@ public abstract class ValidatableProperty { .flatMap(Collection::stream) .toList(); } + + public ValidatableProperty computedBy(final Function compute) { + this.computedBy = compute; + this.computed = true; + return this; + } + + public T compute(final E entity) { + return computedBy.apply(entity); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/Array.java b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java index 39588f11..86a4766a 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/Array.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java @@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.mapper; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -43,4 +44,8 @@ public class Array { .toArray(String[]::new); return joined; } + + public static T[] emptyArray() { + return of(); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java b/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java index 4962ac8d..21153b14 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/PatchableMapWrapper.java @@ -53,13 +53,20 @@ public class PatchableMapWrapper implements Map { } public String toString() { - return "{ " + return "{\n" + ( keySet().stream().sorted() - .map(k -> k + ": " + get(k))) - .collect(joining(", ") + .map(k -> " \"" + k + "\": " + optionallyQuoted(get(k)))) + .collect(joining(",\n") ) - + " }"; + + "\n}\n"; + } + + private Object optionallyQuoted(final Object value) { + if ( value instanceof Number || value instanceof Boolean ) { + return value; + } + return "\"" + value + "\""; } // --- below just delegating methods -------------------------------- 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 903d5385..258b55b7 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 @@ -53,7 +53,7 @@ class HsBookingItemEntityUnitTest { void toStringContainsAllPropertiesAndResourcesSortedByKey() { final var result = givenBookingItem.toString(); - assertThat(result).isEqualTo("HsBookingItemEntity(D-1234500:test project, CLOUD_SERVER, [2020-01-01,2031-01-01), some caption, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })"); + assertThat(result).isEqualToIgnoringWhitespace("HsBookingItemEntity(D-1234500:test project, CLOUD_SERVER, [2020-01-01,2031-01-01), some caption, { \"CPUs\": 2, \"HDD-storage\": 2048, \"SSD-storage\": 512 })"); } @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 f125974a..5e32e23d 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 @@ -170,9 +170,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then allTheseBookingItemsAreReturned( result, - "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 })", - "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 })", - "HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 })"); + "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 } )", + "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 } )", + "HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 } )"); assertThat(result.stream().filter(bi -> bi.getRelatedHostingAsset()!=null).findAny()) .as("at least one relatedProject expected, but none found => fetching relatedProject does not work") .isNotEmpty(); @@ -193,9 +193,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then: exactlyTheseBookingItemsAreReturned( result, - "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 })", - "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 })", - "HsBookingItemEntity(D-1000111:D-1000111 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 })"); + "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 } )", + "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 } )", + "HsBookingItemEntity(D-1000111:D-1000111 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 } )"); } } @@ -348,13 +348,17 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup final List actualResult, final String... bookingItemNames) { assertThat(actualResult) - .extracting(bookingItemEntity -> bookingItemEntity.toString()) + .extracting(HsBookingItemEntity::toString) + .extracting(string-> string.replaceAll("\\s+", " ")) + .extracting(string-> string.replaceAll("\"", "")) .containsExactlyInAnyOrder(bookingItemNames); } void allTheseBookingItemsAreReturned(final List actualResult, final String... bookingItemNames) { assertThat(actualResult) - .extracting(bookingItemEntity -> bookingItemEntity.toString()) + .extracting(HsBookingItemEntity::toString) + .extracting(string -> string.replaceAll("\\s+", " ")) + .extracting(string -> string.replaceAll("\"", "")) .contains(bookingItemNames); } } 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 0779fa2f..b2b43df9 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 @@ -12,21 +12,35 @@ import static net.hostsharing.hsadminng.hs.booking.project.TestHsBookingProject. @UtilityClass public class TestHsBookingItem { - public static final HsBookingItemEntity TEST_MANAGED_SERVER_BOOKING_ITEM = HsBookingItemEntity.builder() - .project(TEST_PROJECT) - .type(HsBookingItemType.MANAGED_SERVER) - .caption("test project booking item") - .resources(Map.ofEntries( - entry("someThing", 1), - entry("anotherThing", "blue") - )) - .validity(Range.closedInfinite(LocalDate.of(2020, 1, 15))) - .build(); - public static final HsBookingItemEntity TEST_CLOUD_SERVER_BOOKING_ITEM = HsBookingItemEntity.builder() .project(TEST_PROJECT) .type(HsBookingItemType.CLOUD_SERVER) .caption("test cloud server booking item") .validity(Range.closedInfinite(LocalDate.of(2020, 1, 15))) .build(); + + public static final HsBookingItemEntity TEST_MANAGED_SERVER_BOOKING_ITEM = HsBookingItemEntity.builder() + .project(TEST_PROJECT) + .type(HsBookingItemType.MANAGED_SERVER) + .caption("test project booking item") + .resources(Map.ofEntries( + entry("CPUs", 2), + entry("RAM", 4), + entry("SSD", 50), + entry("Traffic", 250) + )) + .validity(Range.closedInfinite(LocalDate.of(2020, 1, 15))) + .build(); + + public static final HsBookingItemEntity TEST_MANAGED_WEBSPACE_BOOKING_ITEM = HsBookingItemEntity.builder() + .parentItem(TEST_MANAGED_SERVER_BOOKING_ITEM) + .type(HsBookingItemType.MANAGED_WEBSPACE) + .caption("test managed webspace item") + .resources(Map.ofEntries( + entry("SSD", 50), + entry("Traffic", 250) + )) + .validity(Range.closedInfinite(LocalDate.of(2020, 1, 15))) + .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 0b231bbd..11bfc45c 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 @@ -25,12 +25,14 @@ import org.springframework.transaction.annotation.Transactional; import java.util.HashMap; import java.util.Map; import java.util.UUID; +import java.util.function.Supplier; 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 net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER; import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; +import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.strictlyEquals; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.matchesRegex; @@ -73,7 +75,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup // given context("superuser-alex@hostsharing.net"); final var givenProject = projectRepo.findByCaption("D-1000111 default project").stream() - .findAny().orElseThrow(); + .findAny().orElseThrow(); RestAssured // @formatter:off .given() @@ -264,7 +266,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup void propertyValidationsArePerformend_whenAddingAsset() { context.define("superuser-alex@hostsharing.net"); - final var givenBookingItem = givenSomeNewBookingItem("D-1000111 default project", + final var givenBookingItem = givenSomeNewBookingItem( + "D-1000111 default project", HsBookingItemType.MANAGED_SERVER, "some PrivateCloud"); @@ -292,14 +295,13 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "statusPhrase": "Bad Request", "message": "[ <<<'MANAGED_SERVER:vm1400.config.extra' is not expected but is set to '42', - <<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be <= 100 but is 101, - <<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be >= 10 but is 0 + <<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be at most 100 but is 101, + <<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be at least 10 but is 0 <<<]" } """.replaceAll(" +<<<", ""))); // @formatter:on } - @Test void totalsLimitValidationsArePerformend_whenAddingAsset() { @@ -311,7 +313,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net"); - for (int n = 0; n < 25; ++n ) { + for (int n = 0; n < 25; ++n) { toCleanup(assetRepo.save( HsHostingAssetEntity.builder() .type(UNIX_USER) @@ -358,8 +360,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup void globalAdmin_canGetArbitraryAsset() { context.define("superuser-alex@hostsharing.net"); final var givenAssetUuid = assetRepo.findByIdentifier("vm1011").stream() - .filter(bi -> bi.getBookingItem().getProject().getCaption().equals("D-1000111 default project")) - .findAny().orElseThrow().getUuid(); + .filter(bi -> bi.getBookingItem().getProject().getCaption().equals("D-1000111 default project")) + .findAny().orElseThrow().getUuid(); RestAssured // @formatter:off .given() @@ -429,8 +431,23 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void globalAdmin_canPatchAllUpdatablePropertiesOfAsset() { - final var givenAsset = givenSomeTemporaryHostingAsset("2001", MANAGED_SERVER, - config("monit_max_ssd_usage", 80), config("monit_max_hdd_usage", 90), config("monit_max_cpu_usage", 90), config("monit_max_ram_usage", 70)); + final var givenAsset = givenSomeTemporaryHostingAsset(() -> + HsHostingAssetEntity.builder() + .uuid(UUID.randomUUID()) + .bookingItem(givenSomeNewBookingItem( + "D-1000111 default project", + HsBookingItemType.MANAGED_SERVER, + "temp ManagedServer")) + .type(MANAGED_SERVER) + .identifier("vm2001") + .caption("some test-asset") + .config(Map.ofEntries( + Map.entry("monit_max_ssd_usage", 80), + Map.entry("monit_max_hdd_usage", 90), + Map.entry("monit_max_cpu_usage", 90), + Map.entry("monit_max_ram_usage", 70) + )) + .build()); final var alarmContactUuid = givenContact().getUuid(); RestAssured // @formatter:off @@ -459,9 +476,10 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "identifier": "vm2001", "caption": "some test-asset", "alarmContact": { - "uuid": "%s", "caption": "second contact", - "emailAddresses": { "main": "contact-admin@secondcontact.example.com" } + "emailAddresses": { + "main": "contact-admin@secondcontact.example.com" + } }, "config": { "monit_max_cpu_usage": 90, @@ -470,27 +488,101 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup "monit_min_free_ssd": 5 } } - """.formatted(alarmContactUuid))); + """)); // @formatter:on // finally, the asset is actually updated + em.clear(); context.define("superuser-alex@hostsharing.net"); assertThat(assetRepo.findByUuid(givenAsset.getUuid())).isPresent().get() .matches(asset -> { - assertThat(asset.getAlarmContact().toString()).isEqualTo( - "contact(caption='second contact', emailAddresses='{ main: contact-admin@secondcontact.example.com }')"); - assertThat(asset.getConfig().toString()).isEqualTo( - "{ monit_max_cpu_usage: 90, monit_max_ram_usage: 70, monit_max_ssd_usage: 85, monit_min_free_ssd: 5 }"); + assertThat(asset.getAlarmContact()).isNotNull() + .extracting(c -> c.getEmailAddresses().get("main")) + .isEqualTo("contact-admin@secondcontact.example.com"); + assertThat(asset.getConfig().toString()) + .isEqualToIgnoringWhitespace(""" + { + "monit_max_cpu_usage": 90, + "monit_max_ram_usage": 70, + "monit_max_ssd_usage": 85, + "monit_min_free_ssd": 5 + } + """); return true; }); } - } - private HsOfficeContactEntity givenContact() { - return jpaAttempt.transacted(() -> { - context.define("superuser-alex@hostsharing.net"); - return contactRepo.findContactByOptionalCaptionLike("second").stream().findFirst().orElseThrow(); - }).returnedValue(); + @Test + void assetAdmin_canPatchAllUpdatablePropertiesOfAsset() { + + final var givenAsset = givenSomeTemporaryHostingAsset(() -> + HsHostingAssetEntity.builder() + .uuid(UUID.randomUUID()) + .type(UNIX_USER) + .parentAsset(givenHostingAsset(MANAGED_WEBSPACE, "fir01")) + .identifier("fir01-temp") + .caption("some test-unix-user") + .build()); + + RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + //.header("assumed-roles", "hs_hosting_asset#vm2001:ADMIN") + .contentType(ContentType.JSON) + .body(""" + { + "caption": "some patched test-unix-user", + "config": { + "shell": "/bin/bash", + "totpKey": "0x1234567890abcdef0123456789abcdef", + "password": "Ein Passwort mit 4 Zeichengruppen!" + } + } + """) + .port(port) + .when() + .patch("http://localhost/api/hs/hosting/assets/" + givenAsset.getUuid()) + .then().log().all().assertThat() + .statusCode(200) + .contentType(ContentType.JSON) + .body("", lenientlyEquals(""" + { + "type": "UNIX_USER", + "identifier": "fir01-temp", + "caption": "some patched test-unix-user", + "config": { + "homedir": "/home/pacs/fir01/users/temp", + "shell": "/bin/bash" + } + } + """)) + // the config separately but not-leniently to make sure that no write-only-properties are listed + .body("config", strictlyEquals(""" + { + "homedir": "/home/pacs/fir01/users/temp", + "shell": "/bin/bash" + } + """)) + ; + // @formatter:on + + // finally, the asset is actually updated + assertThat(jpaAttempt.transacted(() -> { + context.define("superuser-alex@hostsharing.net"); + return assetRepo.findByUuid(givenAsset.getUuid()); + }).returnedValue()).isPresent().get() + .matches(asset -> { + assertThat(asset.getCaption()).isEqualTo("some patched test-unix-user"); + assertThat(asset.getConfig().toString()).isEqualTo(""" + { + "password": "Ein Passwort mit 4 Zeichengruppen!", + "shell": "/bin/bash", + "totpKey": "0x1234567890abcdef0123456789abcdef" + } + """); + return true; + }); + } } @Nested @@ -500,9 +592,23 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void globalAdmin_canDeleteArbitraryAsset() { context.define("superuser-alex@hostsharing.net"); - final var givenAsset = givenSomeTemporaryHostingAsset("1002", MANAGED_SERVER, - config("monit_max_ssd_usage", 80), config("monit_max_hdd_usage", 90), config("monit_max_cpu_usage", 90), config("monit_max_ram_usage", 70)); - + final var givenAsset = givenSomeTemporaryHostingAsset(() -> + HsHostingAssetEntity.builder() + .uuid(UUID.randomUUID()) + .bookingItem(givenSomeNewBookingItem( + "D-1000111 default project", + HsBookingItemType.MANAGED_SERVER, + "temp ManagedServer")) + .type(MANAGED_SERVER) + .identifier("vm1002") + .caption("some test-asset") + .config(Map.ofEntries( + Map.entry("monit_max_ssd_usage", 80), + Map.entry("monit_max_hdd_usage", 90), + Map.entry("monit_max_cpu_usage", 90), + Map.entry("monit_max_ram_usage", 70) + )) + .build()); RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") @@ -519,9 +625,23 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup @Test void normalUser_canNotDeleteUnrelatedAsset() { context.define("superuser-alex@hostsharing.net"); - final var givenAsset = givenSomeTemporaryHostingAsset("1003", MANAGED_SERVER, - config("monit_max_ssd_usage", 80), config("monit_max_hdd_usage", 90), config("monit_max_cpu_usage", 90), config("monit_max_ram_usage", 70)); - + final var givenAsset = givenSomeTemporaryHostingAsset(() -> + HsHostingAssetEntity.builder() + .uuid(UUID.randomUUID()) + .bookingItem(givenSomeNewBookingItem( + "D-1000111 default project", + HsBookingItemType.MANAGED_SERVER, + "temp ManagedServer")) + .type(MANAGED_SERVER) + .identifier("vm1003") + .caption("some test-asset") + .config(Map.ofEntries( + Map.entry("monit_max_ssd_usage", 80), + Map.entry("monit_max_hdd_usage", 90), + Map.entry("monit_max_cpu_usage", 90), + Map.entry("monit_max_ram_usage", 70) + )) + .build()); RestAssured // @formatter:off .given() .header("current-user", "selfregistered-user-drew@hostsharing.org") @@ -538,7 +658,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup HsHostingAssetEntity givenHostingAsset(final HsHostingAssetType type, final String identifier) { return assetRepo.findByIdentifier(identifier).stream() - .filter(ha -> ha.getType()==type) + .filter(ha -> ha.getType() == type) .findAny().orElseThrow(); } @@ -559,12 +679,18 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup }).assertSuccessful().returnedValue(); } - HsBookingItemEntity givenSomeNewBookingItem(final String projectCaption, final HsBookingItemType bookingItemType, final String bookingItemCaption) { + HsBookingItemEntity givenSomeNewBookingItem( + final String projectCaption, + final HsBookingItemType bookingItemType, + final String bookingItemCaption) { return jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net"); final var project = projectRepo.findByCaption(projectCaption).getFirst(); final var resources = switch (bookingItemType) { - case MANAGED_SERVER -> Map.ofEntries(entry("CPUs", 1), entry("RAM", 20), entry("SSD", 25), entry("Traffic", 250)); + case MANAGED_SERVER -> Map.ofEntries(entry("CPUs", 1), + entry("RAM", 20), + entry("SSD", 25), + entry("Traffic", 250)); default -> new HashMap(); }; final var newBookingItem = HsBookingItemEntity.builder() @@ -584,33 +710,18 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup return givenAsset; } - @SafeVarargs - private HsHostingAssetEntity givenSomeTemporaryHostingAsset(final String identifierSuffix, - final HsHostingAssetType hostingAssetType, - final Map.Entry... config) { + private HsHostingAssetEntity givenSomeTemporaryHostingAsset(final Supplier newAsset) { return jpaAttempt.transacted(() -> { context.define("superuser-alex@hostsharing.net"); - final var bookingItemType = switch (hostingAssetType) { - case CLOUD_SERVER -> HsBookingItemType.CLOUD_SERVER; - case MANAGED_SERVER -> HsBookingItemType.MANAGED_SERVER; - case MANAGED_WEBSPACE -> HsBookingItemType.MANAGED_WEBSPACE; - default -> null; - }; - final var newBookingItem = givenSomeNewBookingItem("D-1000111 default project", bookingItemType, "temp ManagedServer"); - final var newAsset = HsHostingAssetEntity.builder() - .uuid(UUID.randomUUID()) - .bookingItem(newBookingItem) - .type(hostingAssetType) - .identifier("vm" + identifierSuffix) - .caption("some test-asset") - .config(Map.ofEntries(config)) - .build(); - - return assetRepo.save(newAsset); + return toCleanup(assetRepo.save(newAsset.get())); }).assertSuccessful().returnedValue(); } - private Map.Entry config(final String key, final Object value) { - return entry(key, value); + private HsOfficeContactEntity givenContact() { + return jpaAttempt.transacted(() -> { + context.define("superuser-alex@hostsharing.net"); + return contactRepo.findContactByOptionalCaptionLike("second").stream().findFirst().orElseThrow(); + }).returnedValue(); } + } 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 1dd7c0e1..6460ae39 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 @@ -57,14 +57,14 @@ class HsHostingAssetEntityUnitTest { @Test void toStringContainsAllPropertiesAndResourcesSortedByKey() { - assertThat(givenWebspace.toString()).isEqualTo( - "HsHostingAssetEntity(MANAGED_WEBSPACE, xyz00, some managed webspace, MANAGED_SERVER:vm1234, D-1234500:test project:test cloud server booking item, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })"); + assertThat(givenWebspace.toString()).isEqualToIgnoringWhitespace( + "HsHostingAssetEntity(MANAGED_WEBSPACE, xyz00, some managed webspace, MANAGED_SERVER:vm1234, D-1234500:test project:test cloud server booking item, { \"CPUs\": 2, \"HDD-storage\": 2048, \"SSD-storage\": 512 })"); - assertThat(givenUnixUser.toString()).isEqualTo( - "HsHostingAssetEntity(UNIX_USER, xyz00-web, some unix-user, MANAGED_WEBSPACE:xyz00, { HDD-hard-quota: 512, HDD-soft-quota: 256, SSD-hard-quota: 256, SSD-soft-quota: 128 })"); + assertThat(givenUnixUser.toString()).isEqualToIgnoringWhitespace( + "HsHostingAssetEntity(UNIX_USER, xyz00-web, some unix-user, MANAGED_WEBSPACE:xyz00, { \"HDD-hard-quota\": 512, \"HDD-soft-quota\": 256, \"SSD-hard-quota\": 256, \"SSD-soft-quota\": 128 })"); - assertThat(givenDomainHttpSetup.toString()).isEqualTo( - "HsHostingAssetEntity(DOMAIN_HTTP_SETUP, example.org, some domain setup, MANAGED_WEBSPACE:xyz00, UNIX_USER:xyz00-web, { option-htdocsfallback: true, use-fcgiphpbin: /usr/lib/cgi-bin/php, validsubdomainnames: * })"); + assertThat(givenDomainHttpSetup.toString()).isEqualToIgnoringWhitespace( + "HsHostingAssetEntity(DOMAIN_HTTP_SETUP, example.org, some domain setup, MANAGED_WEBSPACE:xyz00, UNIX_USER:xyz00-web, { \"option-htdocsfallback\": true, \"use-fcgiphpbin\": \"/usr/lib/cgi-bin/php\", \"validsubdomainnames\": \"*\" })"); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java index 9a04c9b4..7910408c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java @@ -59,9 +59,7 @@ class HsHostingAssetPropsControllerAcceptanceTest { "unit": "%", "min": 10, "max": 100, - "required": false, - "defaultValue": 92, - "isTotalsValidator": false + "defaultValue": 92 }, { "type": "integer", @@ -69,9 +67,7 @@ class HsHostingAssetPropsControllerAcceptanceTest { "unit": "%", "min": 10, "max": 100, - "required": false, - "defaultValue": 92, - "isTotalsValidator": false + "defaultValue": 92 }, { "type": "integer", @@ -79,18 +75,14 @@ class HsHostingAssetPropsControllerAcceptanceTest { "unit": "%", "min": 10, "max": 100, - "required": false, - "defaultValue": 98, - "isTotalsValidator": false + "defaultValue": 98 }, { "type": "integer", "propertyName": "monit_min_free_ssd", "min": 1, "max": 1000, - "required": false, - "defaultValue": 5, - "isTotalsValidator": false + "defaultValue": 5 }, { "type": "integer", @@ -98,32 +90,24 @@ class HsHostingAssetPropsControllerAcceptanceTest { "unit": "%", "min": 10, "max": 100, - "required": false, - "defaultValue": 95, - "isTotalsValidator": false + "defaultValue": 95 }, { "type": "integer", "propertyName": "monit_min_free_hdd", "min": 1, "max": 4000, - "required": false, - "defaultValue": 10, - "isTotalsValidator": false + "defaultValue": 10 }, { "type": "boolean", "propertyName": "software-pgsql", - "required": false, - "defaultValue": true, - "isTotalsValidator": false + "defaultValue": true }, { "type": "boolean", "propertyName": "software-mariadb", - "required": false, - "defaultValue": true, - "isTotalsValidator": false + "defaultValue": true }, { "type": "enumeration", @@ -139,114 +123,70 @@ class HsHostingAssetPropsControllerAcceptanceTest { "8.1", "8.2" ], - "required": false, - "defaultValue": "8.2", - "isTotalsValidator": false + "defaultValue": "8.2" }, { "type": "boolean", - "propertyName": "software-php-5.6", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-php-5.6" }, { "type": "boolean", - "propertyName": "software-php-7.0", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-php-7.0" }, { "type": "boolean", - "propertyName": "software-php-7.1", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-php-7.1" }, { "type": "boolean", - "propertyName": "software-php-7.2", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-php-7.2" }, { "type": "boolean", - "propertyName": "software-php-7.3", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-php-7.3" }, { "type": "boolean", "propertyName": "software-php-7.4", - "required": false, - "defaultValue": true, - "isTotalsValidator": false + "defaultValue": true }, { "type": "boolean", - "propertyName": "software-php-8.0", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-php-8.0" }, { "type": "boolean", - "propertyName": "software-php-8.1", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-php-8.1" }, { "type": "boolean", "propertyName": "software-php-8.2", - "required": false, - "defaultValue": true, - "isTotalsValidator": false + "defaultValue": true }, { "type": "boolean", - "propertyName": "software-postfix-tls-1.0", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-postfix-tls-1.0" }, { "type": "boolean", - "propertyName": "software-dovecot-tls-1.0", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-dovecot-tls-1.0" }, { "type": "boolean", "propertyName": "software-clamav", - "required": false, - "defaultValue": true, - "isTotalsValidator": false + "defaultValue": true }, { "type": "boolean", - "propertyName": "software-collabora", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-collabora" }, { "type": "boolean", - "propertyName": "software-libreoffice", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-libreoffice" }, { "type": "boolean", - "propertyName": "software-imagemagick-ghostscript", - "required": false, - "defaultValue": false, - "isTotalsValidator": false + "propertyName": "software-imagemagick-ghostscript" } ] """)); 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 5ada81b0..6c79da67 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 @@ -195,7 +195,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu exactlyTheseAssetsAreReturned( result, "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedWebspace)", - "HsHostingAssetEntity(MANAGED_SERVER, vm1011, some ManagedServer, D-1000111:D-1000111 default project:separate ManagedServer, { monit_max_cpu_usage: 90, monit_max_ram_usage: 80, monit_max_ssd_usage: 70 })"); + "HsHostingAssetEntity(MANAGED_SERVER, vm1011, some ManagedServer, D-1000111:D-1000111 default project:separate ManagedServer, { monit_max_cpu_usage: 90, monit_max_ram_usage: 80, monit_max_ssd_usage: 70 } )"); } @Test @@ -407,6 +407,8 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu final String... serverNames) { assertThat(actualResult) .extracting(HsHostingAssetEntity::toString) + .extracting(input -> input.replaceAll("\\s+", " ")) + .extracting(input -> input.replaceAll("\"", "")) .containsExactlyInAnyOrder(serverNames); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java index 010bbf54..2eb7f581 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java @@ -37,8 +37,8 @@ class HsManagedServerHostingAssetValidatorUnitTest { assertThat(result).containsExactlyInAnyOrder( "'MANAGED_SERVER:vm1234.parentAsset' must be null but is set to D-???????-?:null", "'MANAGED_SERVER:vm1234.assignedToAsset' must be null but is set to D-???????-?:null", - "'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be >= 10 but is 2", - "'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be <= 100 but is 101", + "'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be at least 10 but is 2", + "'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be at most 100 but is 101", "'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'"); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java index afe265b0..8ed76743 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java @@ -1,14 +1,95 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import org.junit.jupiter.api.Test; +import java.util.Map; + +import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_SERVER_BOOKING_ITEM; +import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_WEBSPACE_BOOKING_ITEM; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER; +import static net.hostsharing.hsadminng.mapper.PatchMap.entry; import static org.assertj.core.api.Assertions.assertThat; class HsUnixUserHostingAssetValidatorUnitTest { + private final HsHostingAssetEntity TEST_MANAGED_SERVER_HOSTING_ASSET = HsHostingAssetEntity.builder() + .type(HsHostingAssetType.MANAGED_SERVER) + .identifier("vm1234") + .caption("some managed server") + .bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM) + .build(); + private HsHostingAssetEntity TEST_MANAGED_WEBSPACE_HOSTING_ASSET = HsHostingAssetEntity.builder() + .type(MANAGED_WEBSPACE) + .bookingItem(TEST_MANAGED_WEBSPACE_BOOKING_ITEM) + .parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET) + .identifier("abc00") + .build();; + + @Test + void validatesValidUnixUser() { + // given + final var unixUserHostingAsset = HsHostingAssetEntity.builder() + .type(UNIX_USER) + .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) + .identifier("abc00-temp") + .caption("some valid test UnixUser") + .config(Map.ofEntries( + entry("SSD hard quota", 50), + entry("SSD soft quota", 40), + entry("totpKey", "0x123456789abcdef01234"), + entry("password", "Hallo Computer, lass mich rein!") + )) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); + + // when + final var result = validator.validate(unixUserHostingAsset); + + // then + assertThat(result).isEmpty(); + } + + @Test + void validatesUnixUserProperties() { + // given + final var unixUserHostingAsset = HsHostingAssetEntity.builder() + .type(UNIX_USER) + .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) + .identifier("abc00-temp") + .caption("some test UnixUser with invalid properties") + .config(Map.ofEntries( + entry("SSD hard quota", 100), + entry("SSD soft quota", 200), + entry("HDD hard quota", 100), + entry("HDD soft quota", 200), + entry("shell", "/is/invalid"), + entry("homedir", "/is/read-only"), + entry("totpKey", "should be a hex number"), + entry("password", "short") + )) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); + + // when + final var result = validator.validate(unixUserHostingAsset); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'UNIX_USER:abc00-temp.config.SSD hard quota' is expected to be at most 50 but is 100", + "'UNIX_USER:abc00-temp.config.SSD soft quota' is expected to be at most 100 but is 200", + "'UNIX_USER:abc00-temp.config.HDD hard quota' is expected to be at most 0 but is 100", + "'UNIX_USER:abc00-temp.config.HDD soft quota' is expected to be at most 100 but is 200", + "'UNIX_USER:abc00-temp.config.shell' is expected to be one of [/bin/false, /bin/bash, /bin/csh, /bin/dash, /usr/bin/tcsh, /usr/bin/zsh, /usr/bin/passwd] but is '/is/invalid'", + "'UNIX_USER:abc00-temp.config.homedir' is readonly but given as '/is/read-only'", + "'UNIX_USER:abc00-temp.config.totpKey' is expected to be match ^0x([0-9A-Fa-f]{2})+$ but provided value does not match", + "'UNIX_USER:abc00-temp.config.password' length is expected to be at min 8 but length of provided value is 5", + "'UNIX_USER:abc00-temp.config.password' must contain at least one character of at least 3 of the following groups: upper case letters, lower case letters, digits, special characters" + ); + } + @Test void validatesInvalidIdentifier() { // given @@ -19,7 +100,6 @@ class HsUnixUserHostingAssetValidatorUnitTest { .build(); final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); - // when final var result = validator.validate(unixUserHostingAsset); @@ -27,4 +107,25 @@ class HsUnixUserHostingAssetValidatorUnitTest { assertThat(result).containsExactly( "'identifier' expected to match '^abc00$|^abc00-[a-z0-9]+$', but is 'xyz99-temp'"); } + + @Test + void describesItsProperties() { + // given + final var validator = HsHostingAssetEntityValidatorRegistry.forType(UNIX_USER); + + // when + final var props = validator.properties(); + + // then + assertThat(props).extracting(Object::toString).containsExactlyInAnyOrder( + "{type=integer, propertyName=SSD hard quota, unit=GB, maxFrom=SSD}", + "{type=integer, propertyName=SSD soft quota, unit=GB, maxFrom=SSD hard quota}", + "{type=integer, propertyName=HDD hard quota, unit=GB, maxFrom=HDD}", + "{type=integer, propertyName=HDD soft quota, unit=GB, maxFrom=HDD hard quota}", + "{type=enumeration, propertyName=shell, values=[/bin/false, /bin/bash, /bin/csh, /bin/dash, /usr/bin/tcsh, /usr/bin/zsh, /usr/bin/passwd], defaultValue=/bin/false}", + "{type=string, propertyName=homedir, readOnly=true, computed=true}", + "{type=string, propertyName=totpKey, matchesRegEx=^0x([0-9A-Fa-f]{2})+$, minLength=20, maxLength=256, writeOnly=true, undisclosed=true}", + "{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, undisclosed=true}" + ); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java new file mode 100644 index 00000000..66da5f2d --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java @@ -0,0 +1,92 @@ +package net.hostsharing.hsadminng.hs.validation; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.List; + +import static net.hostsharing.hsadminng.hs.validation.PasswordProperty.passwordProperty; +import static org.assertj.core.api.Assertions.assertThat; + +class PasswordPropertyUnitTest { + + private final ValidatableProperty passwordProp = passwordProperty("password").minLength(8).maxLength(40).writeOnly(); + private final List violations = new ArrayList<>(); + + @ParameterizedTest + @ValueSource(strings = { + "lowerUpperAndDigit1", + "lowerUpperAndSpecial!", + "digit1LowerAndSpecial!", + "digit1special!lower", + "DIGIT1SPECIAL!UPPER" }) + void shouldValidateValidPassword(final String password) { + // when + passwordProp.validate(violations, password, null); + + // then + assertThat(violations).isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = { + "noDigitNoSpecial", + "!!!!!!12345", + "nolower-nodigit", + "nolower1nospecial", + "NOLOWER-NODIGIT", + "NOLOWER1NOSPECIAL" + }) + void shouldRecognizeMissingCharacterGroup(final String givenPassword) { + // when + passwordProp.validate(violations, givenPassword, null); + + // then + assertThat(violations) + .contains("password' must contain at least one character of at least 3 of the following groups: upper case letters, lower case letters, digits, special characters") + .doesNotContain(givenPassword); + } + + @Test + void shouldRecognizeTooShortPassword() { + // given + final String givenPassword = "0123456"; + + // when + passwordProp.validate(violations, givenPassword, null); + + // then + assertThat(violations) + .contains("password' length is expected to be at min 8 but length of provided value is 7") + .doesNotContain(givenPassword); + } + + @Test + void shouldRecognizeTooLongPassowrd() { + // given + final String givenPassword = "password' length is expected to be at max 40 but is 41"; + + // when + passwordProp.validate(violations, givenPassword, null); + + // then + assertThat(violations).contains("password' length is expected to be at max 40 but length of provided value is 54") + .doesNotContain(givenPassword); + } + + @Test + void shouldRecognizeColonInPassword() { + // given + final String givenPassword = "lowerUpper:1234"; + + // when + passwordProp.validate(violations, givenPassword, null); + + // then + assertThat(violations) + .contains("password' must not contain colon (':')") + .doesNotContain(givenPassword); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonMatcher.java b/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonMatcher.java index 54208e4c..22ddead9 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonMatcher.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/test/JsonMatcher.java @@ -9,13 +9,15 @@ import org.json.JSONException; import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONCompareMode; +import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT; + public class JsonMatcher extends BaseMatcher { - private final String expected; + private final String expectedJson; private JSONCompareMode compareMode; - public JsonMatcher(final String expected, final JSONCompareMode compareMode) { - this.expected = expected; + public JsonMatcher(final String expectedJson, final JSONCompareMode compareMode) { + this.expectedJson = expectedJson; this.compareMode = compareMode; } @@ -47,8 +49,8 @@ public class JsonMatcher extends BaseMatcher { return false; } try { - final var actualJson = new ObjectMapper().writeValueAsString(actual); - JSONAssert.assertEquals(expected, actualJson, compareMode); + final var actualJson = new ObjectMapper().enable(INDENT_OUTPUT).writeValueAsString(actual); + JSONAssert.assertEquals(expectedJson, actualJson, compareMode); return true; } catch (final JSONException | JsonProcessingException e) { throw new AssertionError(e); @@ -59,5 +61,4 @@ public class JsonMatcher extends BaseMatcher { public void describeTo(final Description description) { description.appendText("leniently matches JSON"); } - } From 3391ec6cc90e0faeb06c879529adc201c75f6660 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 28 Jun 2024 11:00:15 +0200 Subject: [PATCH 11/18] implement password-hashing (not fully integrated yet) (#67) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/67 Reviewed-by: Timotheus Pokorra --- build.gradle | 1 + .../hsadminng/hash/HashProcessor.java | 107 ++++++++++++++++++ .../HsBookingItemEntityValidator.java | 4 +- .../HsHostingAssetEntityValidator.java | 6 +- .../HsUnixUserHostingAssetValidator.java | 3 +- .../hs/validation/BooleanProperty.java | 4 +- .../hs/validation/EnumerationProperty.java | 10 +- .../hs/validation/HsEntityValidator.java | 10 +- .../hs/validation/IntegerProperty.java | 4 +- .../hs/validation/PasswordProperty.java | 24 +++- .../hs/validation/StringProperty.java | 31 ++--- .../hs/validation/ValidatableProperty.java | 51 +++++---- .../hostsharing/hsadminng/mapper/Array.java | 15 +++ .../hsadminng/hash/HashProcessorUnitTest.java | 41 +++++++ ...UnixUserHostingAssetValidatorUnitTest.java | 2 +- .../validation/PasswordPropertyUnitTest.java | 30 ++++- 16 files changed, 281 insertions(+), 62 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java diff --git a/build.gradle b/build.gradle index 332a5410..a4cc262f 100644 --- a/build.gradle +++ b/build.gradle @@ -66,6 +66,7 @@ dependencies { implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0' implementation 'org.openapitools:jackson-databind-nullable:0.2.6' implementation 'org.apache.commons:commons-text:1.11.0' + implementation 'commons-codec:commons-codec:1.17.0' implementation 'org.modelmapper:modelmapper:3.2.0' implementation 'org.iban4j:iban4j:3.2.7-RELEASE' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0' diff --git a/src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java b/src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java new file mode 100644 index 00000000..d10ee565 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java @@ -0,0 +1,107 @@ +package net.hostsharing.hsadminng.hash; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; + +import lombok.SneakyThrows; + +import jakarta.validation.ValidationException; + +import static net.hostsharing.hsadminng.hash.HashProcessor.Algorithm.SHA512; + +public class HashProcessor { + + private static final SecureRandom secureRandom = new SecureRandom(); + + public enum Algorithm { + SHA512 + } + + private static final Base64.Encoder BASE64 = Base64.getEncoder(); + private static final String SALT_CHARACTERS = + "abcdefghijklmnopqrstuvwxyz" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "0123456789$_"; + + private final MessageDigest generator; + private byte[] saltBytes; + + @SneakyThrows + public static HashProcessor hashAlgorithm(final Algorithm algorithm) { + return new HashProcessor(algorithm); + } + + private HashProcessor(final Algorithm algorithm) throws NoSuchAlgorithmException { + generator = MessageDigest.getInstance(algorithm.name()); + } + + public String generate(final String password) { + final byte[] saltedPasswordDigest = calculateSaltedDigest(password); + final byte[] hashBytes = appendSaltToSaltedDigest(saltedPasswordDigest); + return BASE64.encodeToString(hashBytes); + } + + private byte[] appendSaltToSaltedDigest(final byte[] saltedPasswordDigest) { + final byte[] hashBytes = new byte[saltedPasswordDigest.length + 1 + saltBytes.length]; + System.arraycopy(saltedPasswordDigest, 0, hashBytes, 0, saltedPasswordDigest.length); + hashBytes[saltedPasswordDigest.length] = ':'; + System.arraycopy(saltBytes, 0, hashBytes, saltedPasswordDigest.length+1, saltBytes.length); + return hashBytes; + } + + private byte[] calculateSaltedDigest(final String password) { + generator.reset(); + generator.update(password.getBytes()); + generator.update(saltBytes); + return generator.digest(); + } + + public HashProcessor withSalt(final byte[] saltBytes) { + this.saltBytes = saltBytes; + return this; + } + + public HashProcessor withSalt(final String salt) { + return withSalt(salt.getBytes()); + } + + public HashProcessor withRandomSalt() { + final var stringBuilder = new StringBuilder(16); + for (int i = 0; i < 16; ++i) { + int randomIndex = secureRandom.nextInt(SALT_CHARACTERS.length()); + stringBuilder.append(SALT_CHARACTERS.charAt(randomIndex)); + } + return withSalt(stringBuilder.toString()); + } + + public HashVerifier withHash(final String hash) { + return new HashVerifier(hash); + } + + private static String getLastPart(String input, char delimiter) { + final var lastIndex = input.lastIndexOf(delimiter); + if (lastIndex == -1) { + throw new IllegalArgumentException("cannot determine salt, expected: 'digest:salt', but no ':' found"); + } + return input.substring(lastIndex + 1); + } + + public class HashVerifier { + + private final String hash; + + public HashVerifier(final String hash) { + this.hash = hash; + withSalt(getLastPart(new String(Base64.getDecoder().decode(hash)), ':')); + } + + public void verify(String password) { + final var computedHash = hashAlgorithm(SHA512).withSalt(saltBytes).generate(password); + if ( !computedHash.equals(hash) ) { + throw new ValidationException("invalid password"); + } + } + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java index 315de471..5cd0d71a 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java @@ -16,7 +16,7 @@ import static java.util.Optional.ofNullable; public class HsBookingItemEntityValidator extends HsEntityValidator { - public HsBookingItemEntityValidator(final ValidatableProperty... properties) { + public HsBookingItemEntityValidator(final ValidatableProperty... properties) { super(properties); } @@ -54,7 +54,7 @@ public class HsBookingItemEntityValidator extends HsEntityValidator propDef) { + final ValidatableProperty propDef) { final var propName = propDef.propertyName(); final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse(""); final var totalValue = ofNullable(bookingItem.getSubBookingItems()).orElse(emptyList()) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java index 05bcee97..9f4a6e61 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java @@ -24,7 +24,7 @@ import static java.util.Optional.ofNullable; public abstract class HsHostingAssetEntityValidator extends HsEntityValidator { - static final ValidatableProperty[] NO_EXTRA_PROPERTIES = new ValidatableProperty[0]; + static final ValidatableProperty[] NO_EXTRA_PROPERTIES = new ValidatableProperty[0]; private final HsHostingAssetEntityValidator.BookingItem bookingItemValidation; private final HsHostingAssetEntityValidator.ParentAsset parentAssetValidation; @@ -36,7 +36,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator... properties) { + final ValidatableProperty... properties) { super(properties); this.bookingItemValidation = bookingItemValidation; this.parentAssetValidation = parentAssetValidation; @@ -105,7 +105,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator propDef) { + final ValidatableProperty propDef) { final var propName = propDef.propertyName(); final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse(""); final var totalValue = ofNullable(hostingAsset.getSubHostingAssets()).orElse(emptyList()) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java index 74e59965..1b7b01dc 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java @@ -1,5 +1,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; +import net.hostsharing.hsadminng.hash.HashProcessor; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import net.hostsharing.hsadminng.hs.validation.PropertiesProvider; @@ -30,7 +31,7 @@ class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator { .withDefault("/bin/false"), stringProperty("homedir").readOnly().computedBy(HsUnixUserHostingAssetValidator::computeHomedir), stringProperty("totpKey").matchesRegEx("^0x([0-9A-Fa-f]{2})+$").minLength(20).maxLength(256).undisclosed().writeOnly().optional(), - passwordProperty("password").minLength(8).maxLength(40).writeOnly()); + passwordProperty("password").minLength(8).maxLength(40).hashedUsing(HashProcessor.Algorithm.SHA512).writeOnly()); } @Override diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java index 5f893d74..abe5f7b4 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/BooleanProperty.java @@ -9,7 +9,7 @@ import java.util.Map; import java.util.Objects; @Setter -public class BooleanProperty extends ValidatableProperty { +public class BooleanProperty extends ValidatableProperty { private static final String[] KEY_ORDER = Array.join(ValidatableProperty.KEY_ORDER_HEAD, ValidatableProperty.KEY_ORDER_TAIL); @@ -23,7 +23,7 @@ public class BooleanProperty extends ValidatableProperty { return new BooleanProperty(propertyName); } - public ValidatableProperty falseIf(final String refPropertyName, final String refPropertyValue) { + public BooleanProperty falseIf(final String refPropertyName, final String refPropertyValue) { this.falseIf = new AbstractMap.SimpleImmutableEntry<>(refPropertyName, refPropertyValue); return this; } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java index 60af1b73..60e0f244 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/EnumerationProperty.java @@ -9,7 +9,7 @@ import java.util.List; import static java.util.Arrays.stream; @Setter -public class EnumerationProperty extends ValidatableProperty { +public class EnumerationProperty extends ValidatableProperty { private static final String[] KEY_ORDER = Array.join( ValidatableProperty.KEY_ORDER_HEAD, @@ -26,12 +26,12 @@ public class EnumerationProperty extends ValidatableProperty { return new EnumerationProperty(propertyName); } - public ValidatableProperty values(final String... values) { + public EnumerationProperty values(final String... values) { this.values = values; return this; } - public void deferredInit(final ValidatableProperty[] allProperties) { + public void deferredInit(final ValidatableProperty[] allProperties) { if (hasDeferredInit()) { if (this.values != null) { throw new IllegalStateException("property " + this + " already has values"); @@ -40,8 +40,8 @@ public class EnumerationProperty extends ValidatableProperty { } } - public ValidatableProperty valuesFromProperties(final String propertyNamePrefix) { - this.setDeferredInit( (ValidatableProperty[] allProperties) -> stream(allProperties) + public EnumerationProperty valuesFromProperties(final String propertyNamePrefix) { + this.setDeferredInit( (ValidatableProperty[] allProperties) -> stream(allProperties) .map(ValidatableProperty::propertyName) .filter(name -> name.startsWith(propertyNamePrefix)) .map(name -> name.substring(propertyNamePrefix.length())) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java index 5af7118d..bf755bd2 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java @@ -14,9 +14,9 @@ import static java.util.Collections.emptyList; // TODO.refa: rename to HsEntityProcessor, also subclasses public abstract class HsEntityValidator { - public final ValidatableProperty[] propertyValidators; + public final ValidatableProperty[] propertyValidators; - public HsEntityValidator(final ValidatableProperty... validators) { + public HsEntityValidator(final ValidatableProperty... validators) { propertyValidators = validators; stream(propertyValidators).forEach(p -> p.deferredInit(propertyValidators)); } @@ -68,7 +68,7 @@ public abstract class HsEntityValidator { .orElse(emptyList())); } - protected static Integer getIntegerValueWithDefault0(final ValidatableProperty prop, final Map propValues) { + protected static Integer getIntegerValueWithDefault0(final ValidatableProperty prop, final Map propValues) { final var value = prop.getValue(propValues); if (value instanceof Integer) { return (Integer) value; @@ -92,10 +92,10 @@ public abstract class HsEntityValidator { public Map postProcess(final E entity, final Map config) { final var copy = new HashMap<>(config); stream(propertyValidators).forEach(p -> { + // FIXME: maybe move to ValidatableProperty.postProcess(...)? if ( p.isWriteOnly()) { copy.remove(p.propertyName); - } - if (p.isComputed()) { + } else if (p.isComputed()) { copy.put(p.propertyName, p.compute(entity)); } }); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java index f185c469..7021f9e1 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/IntegerProperty.java @@ -7,7 +7,7 @@ import org.apache.commons.lang3.Validate; import java.util.List; @Setter -public class IntegerProperty extends ValidatableProperty { +public class IntegerProperty extends ValidatableProperty { private final static String[] KEY_ORDER = Array.join( ValidatableProperty.KEY_ORDER_HEAD, @@ -30,7 +30,7 @@ public class IntegerProperty extends ValidatableProperty { } @Override - public void deferredInit(final ValidatableProperty[] allProperties) { + public void deferredInit(final ValidatableProperty[] allProperties) { Validate.isTrue(min == null || minFrom == null, "min and minFrom are exclusive, but both are given"); Validate.isTrue(max == null || maxFrom == null, "max and maxFrom are exclusive, but both are given"); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java index 92cafb9a..6f285595 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java @@ -1,15 +1,24 @@ package net.hostsharing.hsadminng.hs.validation; +import net.hostsharing.hsadminng.hash.HashProcessor.Algorithm; import lombok.Setter; import java.util.List; import java.util.stream.Stream; +import static java.util.Optional.ofNullable; +import static net.hostsharing.hsadminng.hash.HashProcessor.hashAlgorithm; +import static net.hostsharing.hsadminng.mapper.Array.insertAfterEntry; + @Setter -public class PasswordProperty extends StringProperty { +public class PasswordProperty extends StringProperty { + + private static final String[] KEY_ORDER = insertAfterEntry(StringProperty.KEY_ORDER, "computed", "hashedUsing"); + + private Algorithm hashedUsing; private PasswordProperty(final String propertyName) { - super(propertyName); + super(propertyName, KEY_ORDER); undisclosed(); } @@ -23,7 +32,15 @@ public class PasswordProperty extends StringProperty { validatePassword(result, propValue); } - // TODO.impl: only a SHA512 hash should be stored in the database, not the password itself + public PasswordProperty hashedUsing(final Algorithm algorithm) { + this.hashedUsing = algorithm; + // FIXME: computedBy is too late, we need preprocess + computedBy((entity) + -> ofNullable(entity.getDirectValue(propertyName, String.class)) + .map(password -> hashAlgorithm(algorithm).withRandomSalt().generate(password)) + .orElse(null)); + return self(); + } @Override protected String simpleTypeName() { @@ -60,6 +77,5 @@ public class PasswordProperty extends StringProperty { if (containsColon) { result.add(propertyName + "' must not contain colon (':')"); } - } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java index a499d951..a8e8b359 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java @@ -6,10 +6,11 @@ import net.hostsharing.hsadminng.mapper.Array; import java.util.List; import java.util.regex.Pattern; -@Setter -public class StringProperty extends ValidatableProperty { - private static final String[] KEY_ORDER = Array.join( +@Setter +public class StringProperty

> extends ValidatableProperty { + + protected static final String[] KEY_ORDER = Array.join( ValidatableProperty.KEY_ORDER_HEAD, Array.of("matchesRegEx", "minLength", "maxLength"), ValidatableProperty.KEY_ORDER_TAIL, @@ -23,23 +24,27 @@ public class StringProperty extends ValidatableProperty { super(String.class, propertyName, KEY_ORDER); } - public static StringProperty stringProperty(final String propertyName) { - return new StringProperty(propertyName); + protected StringProperty(final String propertyName, final String[] keyOrder) { + super(String.class, propertyName, keyOrder); } - public StringProperty minLength(final int minLength) { + public static StringProperty stringProperty(final String propertyName) { + return new StringProperty<>(propertyName); + } + + public P minLength(final int minLength) { this.minLength = minLength; - return this; + return self(); } - public StringProperty maxLength(final int maxLength) { + public P maxLength(final int maxLength) { this.maxLength = maxLength; - return this; + return self(); } - public StringProperty matchesRegEx(final String regExPattern) { + public P matchesRegEx(final String regExPattern) { this.matchesRegEx = Pattern.compile(regExPattern); - return this; + return self(); } /** @@ -47,9 +52,9 @@ public class StringProperty extends ValidatableProperty { * * @return this; */ - public StringProperty undisclosed() { + public P undisclosed() { this.undisclosed = true; - return this; + return self(); } @Override diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java index b34eb8fa..76fc451e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java @@ -25,7 +25,7 @@ import static java.util.Optional.ofNullable; @Getter @RequiredArgsConstructor -public abstract class ValidatableProperty { +public abstract class ValidatableProperty

, T> { protected static final String[] KEY_ORDER_HEAD = Array.of("propertyName"); protected static final String[] KEY_ORDER_TAIL = Array.of("required", "defaultValue", "readOnly", "writeOnly", "computed", "isTotalsValidator", "thresholdPercentage"); @@ -51,7 +51,7 @@ public abstract class ValidatableProperty { @Accessors(makeFinal = true, chain = true, fluent = false) private boolean writeOnly; - private Function[], T[]> deferredInit; + private Function[], T[]> deferredInit; private boolean isTotalsValidator = false; @JsonIgnore @@ -59,11 +59,16 @@ public abstract class ValidatableProperty { private Integer thresholdPercentage; // TODO.impl: move to IntegerProperty + public final P self() { + //noinspection unchecked + return (P) this; + } + public String unit() { return null; } - protected void setDeferredInit(final Function[], T[]> function) { +protected void setDeferredInit(final Function[], T[]> function) { this.deferredInit = function; } @@ -71,47 +76,47 @@ public abstract class ValidatableProperty { return deferredInit != null; } - public T[] doDeferredInit(final ValidatableProperty[] allProperties) { + public T[] doDeferredInit(final ValidatableProperty[] allProperties) { return deferredInit.apply(allProperties); } - public ValidatableProperty writeOnly() { + public P writeOnly() { this.writeOnly = true; optional(); - return this; + return self(); } - public ValidatableProperty readOnly() { + public P readOnly() { this.readOnly = true; optional(); - return this; + return self(); } - public ValidatableProperty required() { + public P required() { required = TRUE; - return this; + return self(); } - public ValidatableProperty optional() { + public ValidatableProperty optional() { required = FALSE; return this; } - public ValidatableProperty withDefault(final T value) { + public P withDefault(final T value) { defaultValue = value; required = FALSE; - return this; + return self(); } - public void deferredInit(final ValidatableProperty[] allProperties) { + public void deferredInit(final ValidatableProperty[] allProperties) { } - public ValidatableProperty asTotalLimit() { + public P asTotalLimit() { isTotalsValidator = true; - return this; + return self(); } - public ValidatableProperty asTotalLimitFor(final String propertyName, final String propertyValue) { + public P asTotalLimitFor(final String propertyName, final String propertyValue) { if (asTotalLimitValidators == null) { asTotalLimitValidators = new ArrayList<>(); } @@ -132,7 +137,7 @@ public abstract class ValidatableProperty { return emptyList(); }; asTotalLimitValidators.add((final HsBookingItemEntity entity) -> validator.apply(entity, (IntegerProperty)this, 1)); - return this; + return self(); } public String propertyName() { @@ -147,7 +152,7 @@ public abstract class ValidatableProperty { return thresholdPercentage; } - public ValidatableProperty eachComprising(final int factor, final TriFunction> validator) { + public ValidatableProperty eachComprising(final int factor, final TriFunction> validator) { if (asTotalLimitValidators == null) { asTotalLimitValidators = new ArrayList<>(); } @@ -155,9 +160,9 @@ public abstract class ValidatableProperty { return this; } - public ValidatableProperty withThreshold(final Integer percentage) { + public P withThreshold(final Integer percentage) { this.thresholdPercentage = percentage; - return this; + return self(); } public final List validate(final PropertiesProvider propsProvider) { @@ -250,10 +255,10 @@ public abstract class ValidatableProperty { .toList(); } - public ValidatableProperty computedBy(final Function compute) { + public P computedBy(final Function compute) { this.computedBy = compute; this.computed = true; - return this; + return self(); } public T compute(final E entity) { diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/Array.java b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java index 86a4766a..57e76381 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/Array.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java @@ -6,6 +6,8 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import static java.util.Arrays.asList; + /** * Java has List.of(...), Set.of(...) and Map.of(...) all with varargs parameter, * but no Array.of(...). Here it is. @@ -48,4 +50,17 @@ public class Array { public static T[] emptyArray() { return of(); } + + public static T[] insertAfterEntry(final T[] array, final T entryToFind, final T newEntry) { + final var arrayList = new ArrayList<>(asList(array)); + final var index = arrayList.indexOf(entryToFind); + if (index < 0) { + throw new IllegalArgumentException("entry "+ entryToFind + " not found in " + Arrays.toString(array)); + } + arrayList.add(index + 1, newEntry); + + @SuppressWarnings("unchecked") + final var extendedArray = (T[]) java.lang.reflect.Array.newInstance(array.getClass().getComponentType(), array.length); + return arrayList.toArray(extendedArray); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java new file mode 100644 index 00000000..6fc39578 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java @@ -0,0 +1,41 @@ +package net.hostsharing.hsadminng.hash; + +import org.junit.jupiter.api.Test; + +import java.util.Base64; + +import static net.hostsharing.hsadminng.hash.HashProcessor.Algorithm.SHA512; +import static net.hostsharing.hsadminng.hash.HashProcessor.hashAlgorithm; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +class HashProcessorUnitTest { + + final String OTHER_PASSWORD = "other password"; + final String GIVEN_PASSWORD = "given password"; + final String GIVEN_PASSWORD_HASH = "foKDNQP0oZo0pjFpss5vNl0kfHOs6MKMaJUUbpJTg6hqI1WY+KbU/PKQIg2xt/mwDMmW5WR0pdUZnTv8RPTfhjprZUNqTXJsUXczQnczYUxE"; + final String GIVEN_SALT = "given salt"; + + @Test + void verifiesHashedPasswordWithRandomSalt() { + final var hash = hashAlgorithm(SHA512).withRandomSalt().generate(GIVEN_PASSWORD); + hashAlgorithm(SHA512).withHash(hash).verify(GIVEN_PASSWORD); // throws exception if wrong + } + + @Test + void verifiesHashedPasswordWithGivenSalt() { + final var hash = hashAlgorithm(SHA512).withSalt(GIVEN_SALT).generate(GIVEN_PASSWORD); + + final var decoded = new String(Base64.getDecoder().decode(hash)); + assertThat(decoded).endsWith(":" + GIVEN_SALT); + hashAlgorithm(SHA512).withHash(hash).verify(GIVEN_PASSWORD); // throws exception if wrong + } + + @Test + void throwsExceptionForInvalidPassword() { + final var throwable = catchThrowable(() -> + hashAlgorithm(SHA512).withHash(GIVEN_PASSWORD_HASH).verify(OTHER_PASSWORD)); + + assertThat(throwable).hasMessage("invalid password"); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java index 8ed76743..2c92d69b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java @@ -125,7 +125,7 @@ class HsUnixUserHostingAssetValidatorUnitTest { "{type=enumeration, propertyName=shell, values=[/bin/false, /bin/bash, /bin/csh, /bin/dash, /usr/bin/tcsh, /usr/bin/zsh, /usr/bin/passwd], defaultValue=/bin/false}", "{type=string, propertyName=homedir, readOnly=true, computed=true}", "{type=string, propertyName=totpKey, matchesRegEx=^0x([0-9A-Fa-f]{2})+$, minLength=20, maxLength=256, writeOnly=true, undisclosed=true}", - "{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, undisclosed=true}" + "{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, hashedUsing=SHA512, undisclosed=true}" ); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java index 66da5f2d..b694c304 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java @@ -6,13 +6,18 @@ import org.junit.jupiter.params.provider.ValueSource; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import static net.hostsharing.hsadminng.hash.HashProcessor.Algorithm.SHA512; +import static net.hostsharing.hsadminng.hash.HashProcessor.hashAlgorithm; import static net.hostsharing.hsadminng.hs.validation.PasswordProperty.passwordProperty; +import static net.hostsharing.hsadminng.mapper.PatchableMapWrapper.entry; import static org.assertj.core.api.Assertions.assertThat; class PasswordPropertyUnitTest { - private final ValidatableProperty passwordProp = passwordProperty("password").minLength(8).maxLength(40).writeOnly(); + private final ValidatableProperty passwordProp = + passwordProperty("password").minLength(8).maxLength(40).hashedUsing(SHA512).writeOnly(); private final List violations = new ArrayList<>(); @ParameterizedTest @@ -89,4 +94,27 @@ class PasswordPropertyUnitTest { .contains("password' must not contain colon (':')") .doesNotContain(givenPassword); } + + @Test + void shouldComputeHash() { + + // when + final var result = passwordProp.compute(new PropertiesProvider() { + + @Override + public Map directProps() { + return Map.ofEntries( + entry(passwordProp.propertyName, "some password") + ); + } + + @Override + public Object getContextValue(final String propName) { + return null; + } + }); + + // then + hashAlgorithm(SHA512).withHash(result).verify("some password"); // throws exception if wrong + } } From 409f5e97c7c6c9413bc77a1bc195aaec85d40c65 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 1 Jul 2024 15:53:50 +0200 Subject: [PATCH 12/18] integrate-sha512-password-hashing (#68) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/68 Reviewed-by: Marc Sandlus --- build.gradle | 2 +- .../errors/MultiValidationException.java | 2 +- .../hsadminng/hash/HashProcessor.java | 107 ----------------- .../hash/LinuxEtcShadowHashGenerator.java | 112 ++++++++++++++++++ .../HsBookingItemEntityValidator.java | 15 +-- .../HsBookingItemEntityValidatorRegistry.java | 6 +- .../asset/HsHostingAssetController.java | 46 +++---- .../HsHostingAssetEntityProcessor.java | 63 ++++++++++ .../HsHostingAssetEntityValidator.java | 14 ++- ...HsHostingAssetEntityValidatorRegistry.java | 17 --- .../HsUnixUserHostingAssetValidator.java | 4 +- ...OfficeCoopAssetsTransactionController.java | 2 +- ...OfficeCoopSharesTransactionController.java | 2 +- .../hs/validation/HsEntityValidator.java | 20 +++- .../hs/validation/PasswordProperty.java | 7 +- .../hsadminng/hash/HashProcessorUnitTest.java | 41 ------- .../LinuxEtcShadowHashGeneratorUnitTest.java | 51 ++++++++ .../hs/booking/item/TestHsBookingItem.java | 6 + ...oudServerBookingItemValidatorUnitTest.java | 14 +-- ...gedServerBookingItemValidatorUnitTest.java | 18 +-- ...dWebspaceBookingItemValidatorUnitTest.java | 14 +-- ...sHostingAssetControllerAcceptanceTest.java | 4 +- ...udServerHostingAssetValidatorUnitTest.java | 8 +- ...gAssetEntityValidatorRegistryUnitTest.java | 26 ---- ...HsHostingAssetEntityValidatorUnitTest.java | 35 ------ ...edServerHostingAssetValidatorUnitTest.java | 20 ++-- ...WebspaceHostingAssetValidatorUnitTest.java | 14 ++- ...UnixUserHostingAssetValidatorUnitTest.java | 84 +++++++++---- .../validation/PasswordPropertyUnitTest.java | 6 +- 29 files changed, 419 insertions(+), 341 deletions(-) delete mode 100644 src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGenerator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityProcessor.java delete mode 100644 src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGeneratorUnitTest.java delete mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java diff --git a/build.gradle b/build.gradle index a4cc262f..63f4a996 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,7 @@ dependencies { implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0' implementation 'org.openapitools:jackson-databind-nullable:0.2.6' implementation 'org.apache.commons:commons-text:1.11.0' - implementation 'commons-codec:commons-codec:1.17.0' + implementation 'net.java.dev.jna:jna:5.8.0' implementation 'org.modelmapper:modelmapper:3.2.0' implementation 'org.iban4j:iban4j:3.2.7-RELEASE' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0' diff --git a/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java b/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java index a6ba69e8..c8e721a2 100644 --- a/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java +++ b/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java @@ -15,7 +15,7 @@ public class MultiValidationException extends ValidationException { ); } - public static void throwInvalid(final List violations) { + public static void throwIfNotEmpty(final List violations) { if (!violations.isEmpty()) { throw new MultiValidationException(violations); } diff --git a/src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java b/src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java deleted file mode 100644 index d10ee565..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java +++ /dev/null @@ -1,107 +0,0 @@ -package net.hostsharing.hsadminng.hash; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.Base64; - -import lombok.SneakyThrows; - -import jakarta.validation.ValidationException; - -import static net.hostsharing.hsadminng.hash.HashProcessor.Algorithm.SHA512; - -public class HashProcessor { - - private static final SecureRandom secureRandom = new SecureRandom(); - - public enum Algorithm { - SHA512 - } - - private static final Base64.Encoder BASE64 = Base64.getEncoder(); - private static final String SALT_CHARACTERS = - "abcdefghijklmnopqrstuvwxyz" + - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + - "0123456789$_"; - - private final MessageDigest generator; - private byte[] saltBytes; - - @SneakyThrows - public static HashProcessor hashAlgorithm(final Algorithm algorithm) { - return new HashProcessor(algorithm); - } - - private HashProcessor(final Algorithm algorithm) throws NoSuchAlgorithmException { - generator = MessageDigest.getInstance(algorithm.name()); - } - - public String generate(final String password) { - final byte[] saltedPasswordDigest = calculateSaltedDigest(password); - final byte[] hashBytes = appendSaltToSaltedDigest(saltedPasswordDigest); - return BASE64.encodeToString(hashBytes); - } - - private byte[] appendSaltToSaltedDigest(final byte[] saltedPasswordDigest) { - final byte[] hashBytes = new byte[saltedPasswordDigest.length + 1 + saltBytes.length]; - System.arraycopy(saltedPasswordDigest, 0, hashBytes, 0, saltedPasswordDigest.length); - hashBytes[saltedPasswordDigest.length] = ':'; - System.arraycopy(saltBytes, 0, hashBytes, saltedPasswordDigest.length+1, saltBytes.length); - return hashBytes; - } - - private byte[] calculateSaltedDigest(final String password) { - generator.reset(); - generator.update(password.getBytes()); - generator.update(saltBytes); - return generator.digest(); - } - - public HashProcessor withSalt(final byte[] saltBytes) { - this.saltBytes = saltBytes; - return this; - } - - public HashProcessor withSalt(final String salt) { - return withSalt(salt.getBytes()); - } - - public HashProcessor withRandomSalt() { - final var stringBuilder = new StringBuilder(16); - for (int i = 0; i < 16; ++i) { - int randomIndex = secureRandom.nextInt(SALT_CHARACTERS.length()); - stringBuilder.append(SALT_CHARACTERS.charAt(randomIndex)); - } - return withSalt(stringBuilder.toString()); - } - - public HashVerifier withHash(final String hash) { - return new HashVerifier(hash); - } - - private static String getLastPart(String input, char delimiter) { - final var lastIndex = input.lastIndexOf(delimiter); - if (lastIndex == -1) { - throw new IllegalArgumentException("cannot determine salt, expected: 'digest:salt', but no ':' found"); - } - return input.substring(lastIndex + 1); - } - - public class HashVerifier { - - private final String hash; - - public HashVerifier(final String hash) { - this.hash = hash; - withSalt(getLastPart(new String(Base64.getDecoder().decode(hash)), ':')); - } - - public void verify(String password) { - final var computedHash = hashAlgorithm(SHA512).withSalt(saltBytes).generate(password); - if ( !computedHash.equals(hash) ) { - throw new ValidationException("invalid password"); - } - } - } -} diff --git a/src/main/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGenerator.java b/src/main/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGenerator.java new file mode 100644 index 00000000..c030b830 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGenerator.java @@ -0,0 +1,112 @@ +package net.hostsharing.hsadminng.hash; + +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.random.RandomGenerator; + +import com.sun.jna.Library; +import com.sun.jna.Native; + +public class LinuxEtcShadowHashGenerator { + + private static final RandomGenerator random = new SecureRandom(); + private static final Queue predefinedSalts = new PriorityQueue<>(); + + public static final int SALT_LENGTH = 16; + + private final String plaintextPassword; + private Algorithm algorithm; + + public enum Algorithm { + SHA512("6"), + YESCRYPT("y"); + + final String prefix; + + Algorithm(final String prefix) { + this.prefix = prefix; + } + + static Algorithm byPrefix(final String prefix) { + return Arrays.stream(Algorithm.values()).filter(a -> a.prefix.equals(prefix)).findAny() + .orElseThrow(() -> new IllegalArgumentException("unknown hash algorithm: '" + prefix + "'")); + } + } + + private static final String SALT_CHARACTERS = + "abcdefghijklmnopqrstuvwxyz" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "0123456789/."; + + private String salt; + + public static LinuxEtcShadowHashGenerator hash(final String plaintextPassword) { + return new LinuxEtcShadowHashGenerator(plaintextPassword); + } + + private LinuxEtcShadowHashGenerator(final String plaintextPassword) { + this.plaintextPassword = plaintextPassword; + } + + public LinuxEtcShadowHashGenerator using(final Algorithm algorithm) { + this.algorithm = algorithm; + return this; + } + + void verify(final String givenHash) { + final var parts = givenHash.split("\\$"); + if (parts.length < 3 || parts.length > 5) { + throw new IllegalArgumentException("not a " + algorithm.name() + " Linux hash: " + givenHash); + } + + algorithm = Algorithm.byPrefix(parts[1]); + salt = parts.length == 4 ? parts[2] : parts[2] + "$" + parts[3]; + + if (!generate().equals(givenHash)) { + throw new IllegalArgumentException("invalid password"); + } + } + + public String generate() { + if (salt == null) { + throw new IllegalStateException("no salt given"); + } + if (plaintextPassword == null) { + throw new IllegalStateException("no password given"); + } + + return NativeCryptLibrary.INSTANCE.crypt(plaintextPassword, "$" + algorithm.prefix + "$" + salt); + } + + public static void nextSalt(final String salt) { + predefinedSalts.add(salt); + } + + public LinuxEtcShadowHashGenerator withSalt(final String salt) { + this.salt = salt; + return this; + } + + public LinuxEtcShadowHashGenerator withRandomSalt() { + if (!predefinedSalts.isEmpty()) { + return withSalt(predefinedSalts.poll()); + } + final var stringBuilder = new StringBuilder(SALT_LENGTH); + for (int i = 0; i < SALT_LENGTH; ++i) { + int randomIndex = random.nextInt(SALT_CHARACTERS.length()); + stringBuilder.append(SALT_CHARACTERS.charAt(randomIndex)); + } + return withSalt(stringBuilder.toString()); + } + public static void main(String[] args) { + System.out.println(NativeCryptLibrary.INSTANCE.crypt("given password", "$6$abcdefghijklmno")); + } + + public interface NativeCryptLibrary extends Library { + NativeCryptLibrary INSTANCE = Native.load("crypt", NativeCryptLibrary.class); + + String crypt(String password, String salt); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java index 5cd0d71a..82a20e54 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java @@ -20,22 +20,23 @@ public class HsBookingItemEntityValidator extends HsEntityValidator validate(final HsBookingItemEntity bookingItem) { + @Override + public List validateEntity(final HsBookingItemEntity bookingItem) { + return enrich(prefix(bookingItem.toShortString(), "resources"), super.validateProperties(bookingItem)); + } + + @Override + public List validateContext(final HsBookingItemEntity bookingItem) { return sequentiallyValidate( - () -> validateProperties(bookingItem), () -> optionallyValidate(bookingItem.getParentItem()), () -> validateAgainstSubEntities(bookingItem) ); } - private List validateProperties(final HsBookingItemEntity bookingItem) { - return enrich(prefix(bookingItem.toShortString(), "resources"), super.validateProperties(bookingItem)); - } - private static List optionallyValidate(final HsBookingItemEntity bookingItem) { return bookingItem != null ? enrich(prefix(bookingItem.toShortString(), ""), - HsBookingItemEntityValidatorRegistry.doValidate(bookingItem)) + HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem)) : emptyList(); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java index e067781e..388855ff 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java @@ -45,11 +45,13 @@ public class HsBookingItemEntityValidatorRegistry { } public static List doValidate(final HsBookingItemEntity bookingItem) { - return HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validate(bookingItem); + return HsEntityValidator.sequentiallyValidate( + () -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateEntity(bookingItem), + () -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem)); } public static HsBookingItemEntity validated(final HsBookingItemEntity entityToSave) { - MultiValidationException.throwInvalid(doValidate(entityToSave)); + MultiValidationException.throwIfNotEmpty(doValidate(entityToSave)); return entityToSave; } } 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 b0e5cd62..ca4c4a3e 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 @@ -1,6 +1,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; +import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityProcessor; import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetsApi; @@ -21,11 +22,10 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.EntityNotFoundException; import jakarta.persistence.PersistenceContext; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.function.BiConsumer; -import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry.validated; - @RestController public class HsHostingAssetController implements HsHostingAssetsApi { @@ -56,7 +56,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { final var entities = assetRepo.findAllByCriteria(debitorUuid, parentAssetUuid, HsHostingAssetType.of(type)); - final var resources = mapper.mapList(entities, HsHostingAssetResource.class); + final var resources = mapper.mapList(entities, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(resources); } @@ -70,16 +70,21 @@ public class HsHostingAssetController implements HsHostingAssetsApi { context.define(currentUser, assumedRoles); - final var entityToSave = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); + final var entity = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); - final var saved = validated(assetRepo.save(entityToSave)); + final var mapped = new HsHostingAssetEntityProcessor(entity) + .validateEntity() + .prepareForSave() + .saveUsing(assetRepo::save) + .validateContext() + .mapUsing(e -> mapper.map(e, HsHostingAssetResource.class)) + .revampProperties(); final var uri = MvcUriComponentsBuilder.fromController(getClass()) .path("/api/hs/hosting/assets/{id}") - .buildAndExpand(saved.getUuid()) + .buildAndExpand(mapped.getUuid()) .toUri(); - final var mapped = mapper.map(saved, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.created(uri).body(mapped); } @@ -123,21 +128,18 @@ public class HsHostingAssetController implements HsHostingAssetsApi { context.define(currentUser, assumedRoles); - final var current = assetRepo.findByUuid(assetUuid).orElseThrow(); + final var entity = assetRepo.findByUuid(assetUuid).orElseThrow(); - new HsHostingAssetEntityPatcher(em, current).apply(body); + new HsHostingAssetEntityPatcher(em, entity).apply(body); -// TODO.refa: draft for an alternative API -// validate(current) // self-validation, hashing passwords etc. -// .then(HsHostingAssetEntityValidatorRegistry::prepareForSave) // hashing passwords etc. -// .then(assetRepo::save) -// .then(HsHostingAssetEntityValidatorRegistry::validateInContext) -// // In this last step we need the entity and the mapped resource instance, -// // which is exactly what a postmapper takes as arguments. -// .then(this::mapToResource) using postProcessProperties to remove write-only + add read-only properties + final var mapped = new HsHostingAssetEntityProcessor(entity) + .validateEntity() + .prepareForSave() + .saveUsing(assetRepo::save) + .validateContext() + .mapUsing(e -> mapper.map(e, HsHostingAssetResource.class)) + .revampProperties(); - final var saved = validated(assetRepo.save(current)); - final var mapped = mapper.map(saved, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(mapped); } @@ -155,6 +157,8 @@ public class HsHostingAssetController implements HsHostingAssetsApi { } }; - final BiConsumer ENTITY_TO_RESOURCE_POSTMAPPER - = HsHostingAssetEntityValidatorRegistry::postprocessProperties; + @SuppressWarnings("unchecked") + final BiConsumer ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) + -> HsHostingAssetEntityValidatorRegistry.forType(entity.getType()) + .revampProperties(entity, (Map) resource.getConfig()); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityProcessor.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityProcessor.java new file mode 100644 index 00000000..5e270c86 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityProcessor.java @@ -0,0 +1,63 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.errors.MultiValidationException; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource; +import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; + +import java.util.Map; +import java.util.function.Function; + +/** + * Wraps the steps of the pararation, validation, mapping and revamp around saving of a HsHostingAssetEntity into a readable API. + */ +public class HsHostingAssetEntityProcessor { + + private final HsEntityValidator validator; + private HsHostingAssetEntity entity; + private HsHostingAssetResource resource; + + public HsHostingAssetEntityProcessor(final HsHostingAssetEntity entity) { + this.entity = entity; + this.validator = HsHostingAssetEntityValidatorRegistry.forType(entity.getType()); + } + + /// validates the entity itself including its properties + public HsHostingAssetEntityProcessor validateEntity() { + MultiValidationException.throwIfNotEmpty(validator.validateEntity(entity)); + return this; + } + + /// hashing passwords etc. + @SuppressWarnings("unchecked") + public HsHostingAssetEntityProcessor prepareForSave() { + validator.prepareProperties(entity); + return this; + } + + public HsHostingAssetEntityProcessor saveUsing(final Function saveFunction) { + entity = saveFunction.apply(entity); + return this; + } + + /// validates the entity within it's parent and child hierarchy (e.g. totals validators and other limits) + public HsHostingAssetEntityProcessor validateContext() { + MultiValidationException.throwIfNotEmpty(validator.validateContext(entity)); + return this; + } + + /// maps entity to JSON resource representation + public HsHostingAssetEntityProcessor mapUsing( + final Function mapFunction) { + resource = mapFunction.apply(entity); + return this; + } + + /// removes write-only-properties and ads computed-properties + @SuppressWarnings("unchecked") + public HsHostingAssetResource revampProperties() { + final var revampedProps = validator.revampProperties(entity, (Map) resource.getConfig()); + resource.setConfig(revampedProps); + return resource; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java index 9f4a6e61..8508ae1e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java @@ -45,10 +45,16 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator validate(final HsHostingAssetEntity assetEntity) { + public List validateEntity(final HsHostingAssetEntity assetEntity) { return sequentiallyValidate( () -> validateEntityReferencesAndProperties(assetEntity), - () -> validateIdentifierPattern(assetEntity), // might need proper parentAsset or billingItem + () -> validateIdentifierPattern(assetEntity) + ); + } + + @Override + public List validateContext(final HsHostingAssetEntity assetEntity) { + return sequentiallyValidate( () -> optionallyValidate(assetEntity.getBookingItem()), () -> optionallyValidate(assetEntity.getParentAsset()), () -> validateAgainstSubEntities(assetEntity) @@ -82,14 +88,14 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator optionallyValidate(final HsHostingAssetEntity assetEntity) { return assetEntity != null ? enrich(prefix(assetEntity.toShortString(), "parentAsset"), - HsHostingAssetEntityValidatorRegistry.forType(assetEntity.getType()).validate(assetEntity)) + HsHostingAssetEntityValidatorRegistry.forType(assetEntity.getType()).validateContext(assetEntity)) : emptyList(); } private static List optionallyValidate(final HsBookingItemEntity bookingItem) { return bookingItem != null ? enrich(prefix(bookingItem.toShortString(), "bookingItem"), - HsBookingItemEntityValidatorRegistry.doValidate(bookingItem)) + HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem)) : emptyList(); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java index a5331f81..a6c30712 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java @@ -4,7 +4,6 @@ import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource; import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; -import net.hostsharing.hsadminng.errors.MultiValidationException; import java.util.*; @@ -40,22 +39,6 @@ public class HsHostingAssetEntityValidatorRegistry { return validators.keySet(); } - public static List doValidate(final HsHostingAssetEntity hostingAsset) { - final var validator = HsHostingAssetEntityValidatorRegistry.forType(hostingAsset.getType()); - return validator.validate(hostingAsset); - } - - public static HsHostingAssetEntity validated(final HsHostingAssetEntity entityToSave) { - MultiValidationException.throwInvalid(doValidate(entityToSave)); - return entityToSave; - } - - public static void postprocessProperties(final HsHostingAssetEntity entity, final HsHostingAssetResource resource) { - final var validator = HsHostingAssetEntityValidatorRegistry.forType(entity.getType()); - final var config = validator.postProcess(entity, asMap(resource)); - resource.setConfig(config); - } - @SuppressWarnings("unchecked") private static Map asMap(final HsHostingAssetResource resource) { if (resource.getConfig() instanceof Map map) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java index 1b7b01dc..309404f6 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; -import net.hostsharing.hsadminng.hash.HashProcessor; +import net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import net.hostsharing.hsadminng.hs.validation.PropertiesProvider; @@ -31,7 +31,7 @@ class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator { .withDefault("/bin/false"), stringProperty("homedir").readOnly().computedBy(HsUnixUserHostingAssetValidator::computeHomedir), stringProperty("totpKey").matchesRegEx("^0x([0-9A-Fa-f]{2})+$").minLength(20).maxLength(256).undisclosed().writeOnly().optional(), - passwordProperty("password").minLength(8).maxLength(40).hashedUsing(HashProcessor.Algorithm.SHA512).writeOnly()); + passwordProperty("password").minLength(8).maxLength(40).hashedUsing(LinuxEtcShadowHashGenerator.Algorithm.SHA512).writeOnly()); } @Override diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java index 6279ad05..8ec1d956 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java @@ -96,7 +96,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse validateDebitTransaction(requestBody, violations); validateCreditTransaction(requestBody, violations); validateAssetValue(requestBody, violations); - MultiValidationException.throwInvalid(violations); + MultiValidationException.throwIfNotEmpty(violations); } private static void validateDebitTransaction( diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java index f90d5276..78b41c9f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java @@ -98,7 +98,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar validateSubscriptionTransaction(requestBody, violations); validateCancellationTransaction(requestBody, violations); validateshareCount(requestBody, violations); - MultiValidationException.throwInvalid(violations); + MultiValidationException.throwIfNotEmpty(violations); } private static void validateSubscriptionTransaction( diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java index bf755bd2..13cb3f05 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java @@ -32,7 +32,8 @@ public abstract class HsEntityValidator { return String.join(".", parts); } - public abstract List validate(final E entity); + public abstract List validateEntity(final E entity); + public abstract List validateContext(final E entity); public final List> properties() { return Arrays.stream(propertyValidators) @@ -60,7 +61,7 @@ public abstract class HsEntityValidator { } @SafeVarargs - protected static List sequentiallyValidate(final Supplier>... validators) { + public static List sequentiallyValidate(final Supplier>... validators) { return new ArrayList<>(stream(validators) .map(Supplier::get) .filter(violations -> !violations.isEmpty()) @@ -89,13 +90,20 @@ public abstract class HsEntityValidator { throw new IllegalArgumentException("Integer value (or null) expected, but got " + value); } - public Map postProcess(final E entity, final Map config) { + public void prepareProperties(final E entity) { + stream(propertyValidators).forEach(p -> { + if ( p.isWriteOnly() && p.isComputed()) { + entity.directProps().put(p.propertyName, p.compute(entity)); + } + }); + } + + public Map revampProperties(final E entity, final Map config) { final var copy = new HashMap<>(config); stream(propertyValidators).forEach(p -> { - // FIXME: maybe move to ValidatableProperty.postProcess(...)? - if ( p.isWriteOnly()) { + if (p.isWriteOnly()) { copy.remove(p.propertyName); - } else if (p.isComputed()) { + } else if (p.isReadOnly() && p.isComputed()) { copy.put(p.propertyName, p.compute(entity)); } }); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java index 6f285595..37a8146f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java @@ -1,13 +1,13 @@ package net.hostsharing.hsadminng.hs.validation; -import net.hostsharing.hsadminng.hash.HashProcessor.Algorithm; +import net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.Algorithm; import lombok.Setter; import java.util.List; import java.util.stream.Stream; import static java.util.Optional.ofNullable; -import static net.hostsharing.hsadminng.hash.HashProcessor.hashAlgorithm; +import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.hash; import static net.hostsharing.hsadminng.mapper.Array.insertAfterEntry; @Setter @@ -34,10 +34,9 @@ public class PasswordProperty extends StringProperty { public PasswordProperty hashedUsing(final Algorithm algorithm) { this.hashedUsing = algorithm; - // FIXME: computedBy is too late, we need preprocess computedBy((entity) -> ofNullable(entity.getDirectValue(propertyName, String.class)) - .map(password -> hashAlgorithm(algorithm).withRandomSalt().generate(password)) + .map(password -> hash(password).using(algorithm).withRandomSalt().generate()) .orElse(null)); return self(); } diff --git a/src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java deleted file mode 100644 index 6fc39578..00000000 --- a/src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package net.hostsharing.hsadminng.hash; - -import org.junit.jupiter.api.Test; - -import java.util.Base64; - -import static net.hostsharing.hsadminng.hash.HashProcessor.Algorithm.SHA512; -import static net.hostsharing.hsadminng.hash.HashProcessor.hashAlgorithm; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; - -class HashProcessorUnitTest { - - final String OTHER_PASSWORD = "other password"; - final String GIVEN_PASSWORD = "given password"; - final String GIVEN_PASSWORD_HASH = "foKDNQP0oZo0pjFpss5vNl0kfHOs6MKMaJUUbpJTg6hqI1WY+KbU/PKQIg2xt/mwDMmW5WR0pdUZnTv8RPTfhjprZUNqTXJsUXczQnczYUxE"; - final String GIVEN_SALT = "given salt"; - - @Test - void verifiesHashedPasswordWithRandomSalt() { - final var hash = hashAlgorithm(SHA512).withRandomSalt().generate(GIVEN_PASSWORD); - hashAlgorithm(SHA512).withHash(hash).verify(GIVEN_PASSWORD); // throws exception if wrong - } - - @Test - void verifiesHashedPasswordWithGivenSalt() { - final var hash = hashAlgorithm(SHA512).withSalt(GIVEN_SALT).generate(GIVEN_PASSWORD); - - final var decoded = new String(Base64.getDecoder().decode(hash)); - assertThat(decoded).endsWith(":" + GIVEN_SALT); - hashAlgorithm(SHA512).withHash(hash).verify(GIVEN_PASSWORD); // throws exception if wrong - } - - @Test - void throwsExceptionForInvalidPassword() { - final var throwable = catchThrowable(() -> - hashAlgorithm(SHA512).withHash(GIVEN_PASSWORD_HASH).verify(OTHER_PASSWORD)); - - assertThat(throwable).hasMessage("invalid password"); - } -} diff --git a/src/test/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGeneratorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGeneratorUnitTest.java new file mode 100644 index 00000000..c5abcc08 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGeneratorUnitTest.java @@ -0,0 +1,51 @@ +package net.hostsharing.hsadminng.hash; + +import org.junit.jupiter.api.Test; + +import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.Algorithm.SHA512; +import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.hash; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +class LinuxEtcShadowHashGeneratorUnitTest { + + final String GIVEN_PASSWORD = "given password"; + final String WRONG_PASSWORD = "wrong password"; + final String GIVEN_SALT = "0123456789abcdef"; + + // generated via mkpasswd for plaintext password GIVEN_PASSWORD (see above) + final String GIVEN_SHA512_HASH = "$6$ooei1HK6JXVaI7KC$sY5d9fEOr36hjh4CYwIKLMfRKL1539bEmbVCZ.zPiH0sv7jJVnoIXb5YEefEtoSM2WWgDi9hr7vXRe3Nw8zJP/"; + final String GIVEN_YESCRYPT_HASH = "$y$j9T$wgYACPmBXvlMg2MzeZA0p1$KXUzd28nG.67GhPnBZ3aZsNNA5bWFdL/dyG4wS0iRw7"; + + @Test + void verifiesPasswordAgainstSha512HashFromMkpasswd() { + hash(GIVEN_PASSWORD).verify(GIVEN_SHA512_HASH); // throws exception if wrong + } + + @Test + void verifiesPasswordAgainstYescryptHashFromMkpasswd() { + hash(GIVEN_PASSWORD).verify(GIVEN_YESCRYPT_HASH); // throws exception if wrong + } + + @Test + void verifiesHashedPasswordWithRandomSalt() { + final var hash = hash(GIVEN_PASSWORD).using(SHA512).withRandomSalt().generate(); + hash(GIVEN_PASSWORD).verify(hash); // throws exception if wrong + } + + @Test + void verifiesHashedPasswordWithGivenSalt() { + final var givenPasswordHash =hash(GIVEN_PASSWORD).using(SHA512).withSalt(GIVEN_SALT).generate(); + hash(GIVEN_PASSWORD).verify(givenPasswordHash); // throws exception if wrong + } + + @Test + void throwsExceptionForInvalidPassword() { + final var givenPasswordHash = hash(GIVEN_PASSWORD).using(SHA512).withRandomSalt().generate(); + + final var throwable = catchThrowable(() -> + hash(WRONG_PASSWORD).verify(givenPasswordHash) // throws exception if wrong); + ); + assertThat(throwable).hasMessage("invalid password"); + } +} 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 b2b43df9..bcb2baac 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 @@ -16,6 +16,12 @@ public class TestHsBookingItem { .project(TEST_PROJECT) .type(HsBookingItemType.CLOUD_SERVER) .caption("test cloud server booking item") + .resources(Map.ofEntries( + entry("CPUs", 2), + entry("RAM", 4), + entry("SSD", 50), + entry("Traffic", 250) + )) .validity(Range.closedInfinite(LocalDate.of(2020, 1, 15))) .build(); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java index 9258a4a1..b5307cd7 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java @@ -55,13 +55,13 @@ class HsCloudServerBookingItemValidatorUnitTest { // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( - "{type=boolean, propertyName=active, required=false, defaultValue=true, isTotalsValidator=false}", - "{type=integer, propertyName=CPUs, min=1, max=32, required=true, isTotalsValidator=false}", - "{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true, isTotalsValidator=false}", - "{type=integer, propertyName=SSD, unit=GB, min=0, max=1000, step=25, required=true, isTotalsValidator=false}", - "{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, required=false, defaultValue=0, isTotalsValidator=false}", - "{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true, isTotalsValidator=false}", - "{type=enumeration, propertyName=SLA-Infrastructure, values=[BASIC, EXT8H, EXT4H, EXT2H], required=false, isTotalsValidator=false}"); + "{type=boolean, propertyName=active, defaultValue=true}", + "{type=integer, propertyName=CPUs, min=1, max=32, required=true}", + "{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true}", + "{type=integer, propertyName=SSD, unit=GB, min=0, max=1000, step=25, required=true}", + "{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, defaultValue=0}", + "{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true}", + "{type=enumeration, propertyName=SLA-Infrastructure, values=[BASIC, EXT8H, EXT4H, EXT2H]}"); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java index 1fe754ea..11020d92 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java @@ -63,17 +63,17 @@ class HsManagedServerBookingItemValidatorUnitTest { // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( - "{type=integer, propertyName=CPUs, min=1, max=32, required=true, isTotalsValidator=false}", - "{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true, isTotalsValidator=false}", + "{type=integer, propertyName=CPUs, min=1, max=32, required=true}", + "{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true}", "{type=integer, propertyName=SSD, unit=GB, min=25, max=1000, step=25, required=true, isTotalsValidator=true, thresholdPercentage=200}", - "{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, required=false, defaultValue=0, isTotalsValidator=true, thresholdPercentage=200}", + "{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, defaultValue=0, isTotalsValidator=true, thresholdPercentage=200}", "{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true, isTotalsValidator=true, thresholdPercentage=200}", - "{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT8H, EXT4H, EXT2H], required=false, defaultValue=BASIC, isTotalsValidator=false}", - "{type=boolean, propertyName=SLA-EMail, required=false, defaultValue=false, isTotalsValidator=false}", - "{type=boolean, propertyName=SLA-Maria, required=false, isTotalsValidator=false}", - "{type=boolean, propertyName=SLA-PgSQL, required=false, isTotalsValidator=false}", - "{type=boolean, propertyName=SLA-Office, required=false, isTotalsValidator=false}", - "{type=boolean, propertyName=SLA-Web, required=false, isTotalsValidator=false}"); + "{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT8H, EXT4H, EXT2H], defaultValue=BASIC}", + "{type=boolean, propertyName=SLA-EMail}", // TODO.impl: falseIf-validation is missing in output + "{type=boolean, propertyName=SLA-Maria}", + "{type=boolean, propertyName=SLA-PgSQL}", + "{type=boolean, propertyName=SLA-Office}", + "{type=boolean, propertyName=SLA-Web}"); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java index dd9081ee..e75cd551 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java @@ -55,12 +55,12 @@ class HsManagedWebspaceBookingItemValidatorUnitTest { // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( - "{type=integer, propertyName=SSD, unit=GB, min=1, max=100, step=1, required=true, isTotalsValidator=false}", - "{type=integer, propertyName=HDD, unit=GB, min=0, max=250, step=10, required=false, isTotalsValidator=false}", - "{type=integer, propertyName=Traffic, unit=GB, min=10, max=1000, step=10, required=true, isTotalsValidator=false}", - "{type=integer, propertyName=Multi, min=1, max=100, step=1, required=false, defaultValue=1, isTotalsValidator=false}", - "{type=integer, propertyName=Daemons, min=0, max=10, required=false, defaultValue=0, isTotalsValidator=false}", - "{type=boolean, propertyName=Online Office Server, required=false, isTotalsValidator=false}", - "{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT24H], required=false, defaultValue=BASIC, isTotalsValidator=false}"); + "{type=integer, propertyName=SSD, unit=GB, min=1, max=100, step=1, required=true}", + "{type=integer, propertyName=HDD, unit=GB, min=0, max=250, step=10}", + "{type=integer, propertyName=Traffic, unit=GB, min=10, max=1000, step=10, required=true}", + "{type=integer, propertyName=Multi, min=1, max=100, step=1, defaultValue=1}", + "{type=integer, propertyName=Daemons, min=0, max=10, defaultValue=0}", + "{type=boolean, propertyName=Online Office Server}", + "{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT24H], defaultValue=BASIC}"); } } 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 11bfc45c..021fe02a 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 @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset; import io.restassured.RestAssured; import io.restassured.http.ContentType; import net.hostsharing.hsadminng.HsadminNgApplication; +import net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; @@ -523,6 +524,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .identifier("fir01-temp") .caption("some test-unix-user") .build()); + LinuxEtcShadowHashGenerator.nextSalt("Jr5w/Y8zo8pCkqg7"); RestAssured // @formatter:off .given() @@ -575,7 +577,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup assertThat(asset.getCaption()).isEqualTo("some patched test-unix-user"); assertThat(asset.getConfig().toString()).isEqualTo(""" { - "password": "Ein Passwort mit 4 Zeichengruppen!", + "password": "$6$Jr5w/Y8zo8pCkqg7$/rePRbvey3R6Sz/02YTlTQcRt5qdBPTj2h5.hz.rB8NfIoND8pFOjeB7orYcPs9JNf3JDxPP2V.6MQlE5BwAY/", "shell": "/bin/bash", "totpKey": "0x1234567890abcdef0123456789abcdef" } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java index fff0fd56..69fe01bb 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java @@ -29,7 +29,7 @@ class HsCloudServerHostingAssetValidatorUnitTest { // when - final var result = validator.validate(cloudServerHostingAssetEntity); + final var result = validator.validateEntity(cloudServerHostingAssetEntity); // then assertThat(result).containsExactlyInAnyOrder( @@ -49,7 +49,7 @@ class HsCloudServerHostingAssetValidatorUnitTest { // when - final var result = validator.validate(cloudServerHostingAssetEntity); + final var result = validator.validateEntity(cloudServerHostingAssetEntity); // then assertThat(result).containsExactlyInAnyOrder( @@ -76,7 +76,7 @@ class HsCloudServerHostingAssetValidatorUnitTest { final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); // when - final var result = validator.validate(mangedServerHostingAssetEntity); + final var result = validator.validateEntity(mangedServerHostingAssetEntity); // then assertThat(result).containsExactlyInAnyOrder( @@ -96,7 +96,7 @@ class HsCloudServerHostingAssetValidatorUnitTest { final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); // when - final var result = validator.validate(mangedServerHostingAssetEntity); + final var result = validator.validateEntity(mangedServerHostingAssetEntity); // then assertThat(result).containsExactlyInAnyOrder( diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java index 32c098f3..881b5c5f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java @@ -1,16 +1,10 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; -import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import org.junit.jupiter.api.Test; -import java.util.Map; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; -import static org.assertj.core.api.Assertions.entry; class HsHostingAssetEntityValidatorRegistryUnitTest { @@ -41,24 +35,4 @@ class HsHostingAssetEntityValidatorRegistryUnitTest { HsHostingAssetType.UNIX_USER ); } - - @Test - void validatedDoesNotThrowAnExceptionForValidEntity() { - final var givenBookingItem = HsBookingItemEntity.builder() - .type(HsBookingItemType.CLOUD_SERVER) - .resources(Map.ofEntries( - entry("CPUs", 4), - entry("RAM", 20), - entry("SSD", 50), - entry("Traffic", 250) - )) - .build(); - final var validEntity = HsHostingAssetEntity.builder() - .type(HsHostingAssetType.CLOUD_SERVER) - .bookingItem(givenBookingItem) - .identifier("vm1234") - .caption("some valid cloud server") - .build(); - HsHostingAssetEntityValidatorRegistry.validated(validEntity); - } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java deleted file mode 100644 index 73776e89..00000000 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.hostsharing.hsadminng.hs.hosting.asset.validators; - -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; -import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; -import org.junit.jupiter.api.Test; - - -import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; - -class HsHostingAssetEntityValidatorUnitTest { - - @Test - void validThrowsException() { - // given - final var managedServerHostingAssetEntity = HsHostingAssetEntity.builder() - .type(MANAGED_SERVER) - .identifier("vm1234") - .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.MANAGED_SERVER).build()) - .parentAsset(HsHostingAssetEntity.builder().type(MANAGED_SERVER).build()) - .assignedToAsset(HsHostingAssetEntity.builder().type(MANAGED_SERVER).build()) - .build(); - - // when - final var result = catchThrowable( ()-> HsHostingAssetEntityValidatorRegistry.validated(managedServerHostingAssetEntity)); - - // then - assertThat(result.getMessage()).contains( - "'MANAGED_SERVER:vm1234.parentAsset' must be null but is set to D-???????-?:null", - "'MANAGED_SERVER:vm1234.assignedToAsset' must be null but is set to D-???????-?:null" - ); - } -} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java index 2eb7f581..fd8d4800 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java @@ -8,6 +8,8 @@ import org.junit.jupiter.api.Test; import java.util.Map; import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_CLOUD_SERVER_BOOKING_ITEM; +import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_SERVER_BOOKING_ITEM; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; import static org.assertj.core.api.Assertions.assertThat; @@ -19,7 +21,7 @@ class HsManagedServerHostingAssetValidatorUnitTest { final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_SERVER) .identifier("vm1234") - .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.MANAGED_SERVER).build()) + .bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM) .parentAsset(HsHostingAssetEntity.builder().build()) .assignedToAsset(HsHostingAssetEntity.builder().build()) .config(Map.ofEntries( @@ -31,12 +33,12 @@ class HsManagedServerHostingAssetValidatorUnitTest { final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedWebspaceHostingAssetEntity.getType()); // when - final var result = validator.validate(mangedWebspaceHostingAssetEntity); + final var result = validator.validateEntity(mangedWebspaceHostingAssetEntity); // then assertThat(result).containsExactlyInAnyOrder( - "'MANAGED_SERVER:vm1234.parentAsset' must be null but is set to D-???????-?:null", - "'MANAGED_SERVER:vm1234.assignedToAsset' must be null but is set to D-???????-?:null", + "'MANAGED_SERVER:vm1234.parentAsset' must be null but is set to D-1234500:test project:test project booking item", + "'MANAGED_SERVER:vm1234.assignedToAsset' must be null but is set to D-1234500:test project:test project booking item", "'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be at least 10 but is 2", "'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be at most 100 but is 101", "'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'"); @@ -53,7 +55,7 @@ class HsManagedServerHostingAssetValidatorUnitTest { final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); // when - final var result = validator.validate(mangedServerHostingAssetEntity); + final var result = validator.validateEntity(mangedServerHostingAssetEntity); // then assertThat(result).containsExactlyInAnyOrder( @@ -68,17 +70,17 @@ class HsManagedServerHostingAssetValidatorUnitTest { .identifier("xyz00") .parentAsset(HsHostingAssetEntity.builder().build()) .assignedToAsset(HsHostingAssetEntity.builder().build()) - .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build()) + .bookingItem(TEST_CLOUD_SERVER_BOOKING_ITEM) .build(); final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); // when - final var result = validator.validate(mangedServerHostingAssetEntity); + final var result = validator.validateEntity(mangedServerHostingAssetEntity); // then assertThat(result).containsExactlyInAnyOrder( "'MANAGED_SERVER:xyz00.bookingItem' must be of type MANAGED_SERVER but is of type CLOUD_SERVER", - "'MANAGED_SERVER:xyz00.parentAsset' must be null but is set to D-???????-?:null", - "'MANAGED_SERVER:xyz00.assignedToAsset' must be null but is set to D-???????-?:null"); + "'MANAGED_SERVER:xyz00.parentAsset' must be null but is set to D-1234500:test project:test cloud server booking item", + "'MANAGED_SERVER:xyz00.assignedToAsset' must be null but is set to D-1234500:test project:test cloud server booking item"); } } 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 7b981b68..1d2c6d24 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 @@ -7,6 +7,7 @@ import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import org.junit.jupiter.api.Test; import java.util.Map; +import java.util.stream.Stream; import static java.util.Map.entry; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; @@ -70,7 +71,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { .build(); // when - final var result = validator.validate(mangedWebspaceHostingAssetEntity); + final var result = validator.validateContext(mangedWebspaceHostingAssetEntity); // then assertThat(result).isEmpty(); @@ -88,7 +89,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { .build(); // when - final var result = validator.validate(mangedWebspaceHostingAssetEntity); + final var result = validator.validateEntity(mangedWebspaceHostingAssetEntity); // then assertThat(result).containsExactly("'identifier' expected to match '^abc[0-9][0-9]$', but is 'xyz00'"); @@ -109,7 +110,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { .build(); // when - final var result = validator.validate(mangedWebspaceHostingAssetEntity); + final var result = validator.validateEntity(mangedWebspaceHostingAssetEntity); // then assertThat(result).containsExactly("'MANAGED_WEBSPACE:abc00.config.unknown' is not expected but is set to 'some value'"); @@ -131,7 +132,10 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { .build(); // when - final var result = validator.validate(mangedWebspaceHostingAssetEntity); + final var result = Stream.concat( + validator.validateEntity(mangedWebspaceHostingAssetEntity).stream(), + validator.validateContext(mangedWebspaceHostingAssetEntity).stream()) + .toList(); // then assertThat(result).isEmpty(); @@ -154,7 +158,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { .build(); // when - final var result = validator.validate(mangedWebspaceHostingAssetEntity); + final var result = validator.validateEntity(mangedWebspaceHostingAssetEntity); // then assertThat(result).containsExactly( diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java index 2c92d69b..5ef61da9 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java @@ -1,11 +1,14 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; +import net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import org.junit.jupiter.api.Test; -import java.util.Map; +import java.util.HashMap; +import java.util.stream.Stream; +import static java.util.Map.ofEntries; import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_SERVER_BOOKING_ITEM; import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_WEBSPACE_BOOKING_ITEM; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; @@ -21,32 +24,55 @@ class HsUnixUserHostingAssetValidatorUnitTest { .caption("some managed server") .bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM) .build(); - private HsHostingAssetEntity TEST_MANAGED_WEBSPACE_HOSTING_ASSET = HsHostingAssetEntity.builder() + private final HsHostingAssetEntity TEST_MANAGED_WEBSPACE_HOSTING_ASSET = HsHostingAssetEntity.builder() .type(MANAGED_WEBSPACE) .bookingItem(TEST_MANAGED_WEBSPACE_BOOKING_ITEM) .parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET) .identifier("abc00") - .build();; + .build(); + private final HsHostingAssetEntity GIVEN_VALID_UNIX_USER_HOSTING_ASSET = HsHostingAssetEntity.builder() + .type(UNIX_USER) + .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) + .identifier("abc00-temp") + .caption("some valid test UnixUser") + .config(new HashMap<>(ofEntries( + entry("SSD hard quota", 50), + entry("SSD soft quota", 40), + entry("totpKey", "0x123456789abcdef01234"), + entry("password", "Hallo Computer, lass mich rein!") + ))) + .build(); + + @Test + void preparesUnixUser() { + // given + final var unixUserHostingAsset = GIVEN_VALID_UNIX_USER_HOSTING_ASSET; + final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); + + // when + LinuxEtcShadowHashGenerator.nextSalt("Ly3LbsArtL5u4EVt"); + validator.prepareProperties(unixUserHostingAsset); + + // then + assertThat(unixUserHostingAsset.getConfig()).containsExactlyInAnyOrderEntriesOf(ofEntries( + entry("SSD hard quota", 50), + entry("SSD soft quota", 40), + entry("totpKey", "0x123456789abcdef01234"), + entry("password", "$6$Ly3LbsArtL5u4EVt$i/ayIEvm0y4bjkFB6wbg8imbRIaw4mAA4gqYRVyoSkj.iIxJKS3KiRkSjP8gweNcpKL0Q0N31EadT8fCnWErL.") + )); + } @Test void validatesValidUnixUser() { // given - final var unixUserHostingAsset = HsHostingAssetEntity.builder() - .type(UNIX_USER) - .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) - .identifier("abc00-temp") - .caption("some valid test UnixUser") - .config(Map.ofEntries( - entry("SSD hard quota", 50), - entry("SSD soft quota", 40), - entry("totpKey", "0x123456789abcdef01234"), - entry("password", "Hallo Computer, lass mich rein!") - )) - .build(); + final var unixUserHostingAsset = GIVEN_VALID_UNIX_USER_HOSTING_ASSET; final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); // when - final var result = validator.validate(unixUserHostingAsset); + final var result = Stream.concat( + validator.validateEntity(unixUserHostingAsset).stream(), + validator.validateContext(unixUserHostingAsset).stream() + ).toList(); // then assertThat(result).isEmpty(); @@ -60,7 +86,7 @@ class HsUnixUserHostingAssetValidatorUnitTest { .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) .identifier("abc00-temp") .caption("some test UnixUser with invalid properties") - .config(Map.ofEntries( + .config(ofEntries( entry("SSD hard quota", 100), entry("SSD soft quota", 200), entry("HDD hard quota", 100), @@ -74,7 +100,7 @@ class HsUnixUserHostingAssetValidatorUnitTest { final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); // when - final var result = validator.validate(unixUserHostingAsset); + final var result = validator.validateEntity(unixUserHostingAsset); // then assertThat(result).containsExactlyInAnyOrder( @@ -101,13 +127,31 @@ class HsUnixUserHostingAssetValidatorUnitTest { final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); // when - final var result = validator.validate(unixUserHostingAsset); + final var result = validator.validateEntity(unixUserHostingAsset); // then assertThat(result).containsExactly( "'identifier' expected to match '^abc00$|^abc00-[a-z0-9]+$', but is 'xyz99-temp'"); } + @Test + void revampsUnixUser() { + // given + final var unixUserHostingAsset = GIVEN_VALID_UNIX_USER_HOSTING_ASSET; + final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); + + // when + LinuxEtcShadowHashGenerator.nextSalt("Ly3LbsArtL5u4EVt"); + final var result = validator.revampProperties(unixUserHostingAsset, unixUserHostingAsset.getConfig()); + + // then + assertThat(result).containsExactlyInAnyOrderEntriesOf(ofEntries( + entry("SSD hard quota", 50), + entry("SSD soft quota", 40), + entry("homedir", "/home/pacs/abc00/users/temp") + )); + } + @Test void describesItsProperties() { // given @@ -125,7 +169,7 @@ class HsUnixUserHostingAssetValidatorUnitTest { "{type=enumeration, propertyName=shell, values=[/bin/false, /bin/bash, /bin/csh, /bin/dash, /usr/bin/tcsh, /usr/bin/zsh, /usr/bin/passwd], defaultValue=/bin/false}", "{type=string, propertyName=homedir, readOnly=true, computed=true}", "{type=string, propertyName=totpKey, matchesRegEx=^0x([0-9A-Fa-f]{2})+$, minLength=20, maxLength=256, writeOnly=true, undisclosed=true}", - "{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, hashedUsing=SHA512, undisclosed=true}" + "{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, computed=true, hashedUsing=SHA512, undisclosed=true}" ); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java index b694c304..2350b288 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java @@ -8,8 +8,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import static net.hostsharing.hsadminng.hash.HashProcessor.Algorithm.SHA512; -import static net.hostsharing.hsadminng.hash.HashProcessor.hashAlgorithm; +import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.Algorithm.SHA512; +import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.hash; import static net.hostsharing.hsadminng.hs.validation.PasswordProperty.passwordProperty; import static net.hostsharing.hsadminng.mapper.PatchableMapWrapper.entry; import static org.assertj.core.api.Assertions.assertThat; @@ -115,6 +115,6 @@ class PasswordPropertyUnitTest { }); // then - hashAlgorithm(SHA512).withHash(result).verify("some password"); // throws exception if wrong + hash("some password").using(SHA512).withRandomSalt().generate(); // throws exception if wrong } } From c5722e494f8a33e7320c3a8349323b2e6d343f93 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 3 Jul 2024 10:36:29 +0200 Subject: [PATCH 13/18] fix HsHostingAssetRepository.findAllByCriteriaImpl query (#69) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/69 Reviewed-by: Marc Sandlus --- .../asset/HsHostingAssetRepository.java | 26 ++++++++++++++----- .../hs-hosting/hs-hosting-assets.yaml | 4 +-- ...sHostingAssetControllerAcceptanceTest.java | 14 +--------- ...HostingAssetRepositoryIntegrationTest.java | 16 ++++-------- 4 files changed, 28 insertions(+), 32 deletions(-) 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 cefe79f6..571f484a 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 @@ -14,12 +14,26 @@ public interface HsHostingAssetRepository extends Repository findByIdentifier(String assetIdentifier); - @Query(""" - SELECT asset FROM HsHostingAssetEntity asset - WHERE (:projectUuid IS NULL OR asset.bookingItem.project.uuid = :projectUuid) - AND (:parentAssetUuid IS NULL OR asset.parentAsset.uuid = :parentAssetUuid) - AND (:type IS NULL OR :type = CAST(asset.type AS String)) - """) + @Query(value = """ + select ha.uuid, + ha.alarmcontactuuid, + ha.assignedtoassetuuid, + ha.bookingitemuuid, + ha.caption, + ha.config, + ha.identifier, + ha.parentassetuuid, + ha.type, + ha.version + from hs_hosting_asset_rv ha + left join hs_booking_item bi on bi.uuid = ha.bookingitemuuid + left join hs_hosting_asset pha on pha.uuid = ha.parentassetuuid + where (:projectUuid is null or bi.projectuuid=:projectUuid) + and (:parentAssetUuid is null or pha.uuid=:parentAssetUuid) + and (:type is null or :type=cast(ha.type as text)) + """, nativeQuery = true) + // The JPQL query did not generate "left join" but just "join". + // I also optimized the query by not using the _rv for hs_booking_item and hs_hosting_asset, only for hs_hosting_asset_rv. 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)); diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-assets.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-assets.yaml index a08a36a1..8a208c68 100644 --- a/src/main/resources/api-definition/hs-hosting/hs-hosting-assets.yaml +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-assets.yaml @@ -7,13 +7,13 @@ get: parameters: - $ref: 'auth.yaml#/components/parameters/currentUser' - $ref: 'auth.yaml#/components/parameters/assumedRoles' - - name: debitorUuid + - name: projectUuid in: query required: false schema: type: string format: uuid - description: The UUID of the debitor, whose hosting assets are to be listed. + description: The UUID of the project, whose hosting assets are to be listed. - name: parentAssetUuid in: query required: false 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 021fe02a..89de41bc 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 @@ -89,22 +89,10 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .contentType("application/json") .body("", lenientlyEquals(""" [ - { - "type": "MANAGED_WEBSPACE", - "identifier": "sec01", - "caption": "some Webspace", - "config": {} - }, { "type": "MANAGED_WEBSPACE", "identifier": "fir01", - "caption": "some Webspace", - "config": {} - }, - { - "type": "MANAGED_WEBSPACE", - "identifier": "thi01", - "caption": "some Webspace", + "caption": "some Webspace", "config": {} } ] 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 6c79da67..cc8a029b 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 @@ -174,7 +174,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu final var result = assetRepo.findAllByCriteria(null, null, MANAGED_WEBSPACE); // then - allTheseServersAreReturned( + exactlyTheseAssetsAreReturned( result, "HsHostingAssetEntity(MANAGED_WEBSPACE, sec01, some Webspace, MANAGED_SERVER:vm1012, D-1000212:D-1000212 default project:separate ManagedWebspace)", "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedWebspace)", @@ -202,18 +202,19 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu public void normalUser_canFilterAssetsRelatedToParentAsset() { // given context("superuser-alex@hostsharing.net"); - final var parentAssetUuid = assetRepo.findAllByCriteria(null, null, MANAGED_SERVER).stream() + final var parentAssetUuid = assetRepo.findByIdentifier("vm1012").stream() + .filter(ha -> ha.getType() == MANAGED_SERVER) .findAny().orElseThrow().getUuid(); // when + context("superuser-alex@hostsharing.net", "hs_hosting_asset#vm1012:AGENT"); final var result = assetRepo.findAllByCriteria(null, parentAssetUuid, null); // then - allTheseServersAreReturned( + exactlyTheseAssetsAreReturned( result, "HsHostingAssetEntity(MANAGED_WEBSPACE, sec01, some Webspace, MANAGED_SERVER:vm1012, D-1000212:D-1000212 default project:separate ManagedWebspace)"); } - } @Nested @@ -411,11 +412,4 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu .extracting(input -> input.replaceAll("\"", "")) .containsExactlyInAnyOrder(serverNames); } - - void allTheseServersAreReturned(final List actualResult, final String... serverNames) { - assertThat(actualResult) - .extracting(HsHostingAssetEntity::toString) - .contains(serverNames); - actualResult.forEach(loadedEntity -> assertThat(loadedEntity.isLoaded()).isTrue()); - } } From a77eaefb94c43174f47d96abddb3212d4bfa1997 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 3 Jul 2024 11:43:08 +0200 Subject: [PATCH 14/18] add-email-alias-hosting-asset (#70) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/70 Reviewed-by: Marc Sandlus --- .../asset/HsHostingAssetController.java | 4 +- .../HsEMailAliasHostingAssetValidator.java | 33 +++ ...HsHostingAssetEntityValidatorRegistry.java | 1 + .../hs/validation/ArrayProperty.java | 63 +++++ .../hs/validation/PasswordProperty.java | 4 +- .../hs/validation/StringProperty.java | 14 +- .../hs/validation/ValidatableProperty.java | 14 +- .../hostsharing/hsadminng/mapper/Array.java | 7 +- .../7018-hs-hosting-asset-test-data.sql | 1 + ...sHostingAssetControllerAcceptanceTest.java | 40 +-- .../HsHostingAssetControllerRestTest.java | 243 ++++++++++++++++++ ...ingAssetPropsControllerAcceptanceTest.java | 3 +- .../asset/TestHsHostingAssetEntities.java | 22 ++ ...ailAliasHostingAssetValidatorUnitTest.java | 114 ++++++++ ...gAssetEntityValidatorRegistryUnitTest.java | 3 +- ...UnixUserHostingAssetValidatorUnitTest.java | 4 +- 16 files changed, 524 insertions(+), 46 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/validation/ArrayProperty.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/TestHsHostingAssetEntities.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java 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 ca4c4a3e..d9b6492f 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 @@ -159,6 +159,6 @@ public class HsHostingAssetController implements HsHostingAssetsApi { @SuppressWarnings("unchecked") final BiConsumer ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) - -> HsHostingAssetEntityValidatorRegistry.forType(entity.getType()) - .revampProperties(entity, (Map) resource.getConfig()); + -> resource.setConfig(HsHostingAssetEntityValidatorRegistry.forType(entity.getType()) + .revampProperties(entity, (Map) resource.getConfig())); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidator.java new file mode 100644 index 00000000..d151b49d --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidator.java @@ -0,0 +1,33 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; + +import java.util.regex.Pattern; + +import static net.hostsharing.hsadminng.hs.validation.ArrayProperty.arrayOf; +import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; + +class HsEMailAliasHostingAssetValidator extends HsHostingAssetEntityValidator { + + private static final String UNIX_USER_REGEX = "^[a-z][a-z0-9]{2}[0-9]{2}(-[a-z0-9]+)?$"; // also accepts legacy pac-names + private static final String EMAIL_ADDRESS_REGEX = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$"; // RFC 5322 + public static final int EMAIL_ADDRESS_MAX_LENGTH = 320; // according to RFC 5321 and RFC 5322 + + HsEMailAliasHostingAssetValidator() { + super( BookingItem.mustBeNull(), + ParentAsset.mustBeOfType(HsHostingAssetType.MANAGED_WEBSPACE), + AssignedToAsset.mustBeNull(), + AlarmContact.isOptional(), + + arrayOf( + stringProperty("target").maxLength(EMAIL_ADDRESS_MAX_LENGTH).matchesRegEx(UNIX_USER_REGEX, EMAIL_ADDRESS_REGEX) + ).required().minLength(1)); + } + + @Override + protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { + final var webspaceIdentifier = assetEntity.getParentAsset().getIdentifier(); + return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"-[a-z0-9]+$"); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java index a6c30712..a30108e7 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java @@ -19,6 +19,7 @@ public class HsHostingAssetEntityValidatorRegistry { register(MANAGED_SERVER, new HsManagedServerHostingAssetValidator()); register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator()); register(UNIX_USER, new HsUnixUserHostingAssetValidator()); + register(EMAIL_ALIAS, new HsEMailAliasHostingAssetValidator()); } private static void register(final Enum type, final HsEntityValidator validator) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ArrayProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ArrayProperty.java new file mode 100644 index 00000000..9001ea81 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ArrayProperty.java @@ -0,0 +1,63 @@ +package net.hostsharing.hsadminng.hs.validation; + +import lombok.Setter; + +import java.util.Arrays; +import java.util.List; + +import static java.util.Arrays.stream; +import static net.hostsharing.hsadminng.mapper.Array.insertNewEntriesAfterExistingEntry; + +@Setter +public class ArrayProperty

, E> extends ValidatableProperty, E[]> { + + private static final String[] KEY_ORDER = + insertNewEntriesAfterExistingEntry( + insertNewEntriesAfterExistingEntry(ValidatableProperty.KEY_ORDER, "required", "minLength" ,"maxLength"), + "propertyName", "elementsOf"); + private final ValidatableProperty elementsOf; + private Integer minLength; + private Integer maxLength; + + private ArrayProperty(final ValidatableProperty elementsOf) { + //noinspection unchecked + super((Class) elementsOf.type.arrayType(), elementsOf.propertyName, KEY_ORDER); + this.elementsOf = elementsOf; + } + + public static ArrayProperty arrayOf(final ValidatableProperty elementsOf) { + //noinspection unchecked + return (ArrayProperty) new ArrayProperty<>(elementsOf); + } + + public ValidatableProperty minLength(final int minLength) { + this.minLength = minLength; + return self(); + } + + public ValidatableProperty maxLength(final int maxLength) { + this.maxLength = maxLength; + return self(); + } + + @Override + protected void validate(final List result, final E[] propValue, final PropertiesProvider propProvider) { + if (minLength != null && propValue.length < minLength) { + result.add(propertyName + "' length is expected to be at min " + minLength + " but length of " + display(propValue) + " is " + propValue.length); + } + if (maxLength != null && propValue.length > maxLength) { + result.add(propertyName + "' length is expected to be at max " + maxLength + " but length of " + display(propValue) + " is " + propValue.length); + } + stream(propValue).forEach(e -> elementsOf.validate(result, e, propProvider)); + } + + @Override + protected String simpleTypeName() { + return elementsOf.simpleTypeName() + "[]"; + } + + @SafeVarargs + private String display(final E... propValue) { + return "[" + Arrays.toString(propValue) + "]"; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java index 37a8146f..83cdf975 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java @@ -8,12 +8,12 @@ import java.util.stream.Stream; import static java.util.Optional.ofNullable; import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.hash; -import static net.hostsharing.hsadminng.mapper.Array.insertAfterEntry; +import static net.hostsharing.hsadminng.mapper.Array.insertNewEntriesAfterExistingEntry; @Setter public class PasswordProperty extends StringProperty { - private static final String[] KEY_ORDER = insertAfterEntry(StringProperty.KEY_ORDER, "computed", "hashedUsing"); + private static final String[] KEY_ORDER = insertNewEntriesAfterExistingEntry(StringProperty.KEY_ORDER, "computed", "hashedUsing"); private Algorithm hashedUsing; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java index a8e8b359..a92af7f8 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/StringProperty.java @@ -3,9 +3,12 @@ package net.hostsharing.hsadminng.hs.validation; import lombok.Setter; import net.hostsharing.hsadminng.mapper.Array; +import java.util.Arrays; import java.util.List; +import java.util.regex.Matcher; import java.util.regex.Pattern; +import static java.util.Arrays.stream; @Setter public class StringProperty

> extends ValidatableProperty { @@ -15,7 +18,7 @@ public class StringProperty

> extends ValidatableProp Array.of("matchesRegEx", "minLength", "maxLength"), ValidatableProperty.KEY_ORDER_TAIL, Array.of("undisclosed")); - private Pattern matchesRegEx; + private Pattern[] matchesRegEx; private Integer minLength; private Integer maxLength; private boolean undisclosed; @@ -42,8 +45,8 @@ public class StringProperty

> extends ValidatableProp return self(); } - public P matchesRegEx(final String regExPattern) { - this.matchesRegEx = Pattern.compile(regExPattern); + public P matchesRegEx(final String... regExPattern) { + this.matchesRegEx = stream(regExPattern).map(Pattern::compile).toArray(Pattern[]::new); return self(); } @@ -65,8 +68,9 @@ public class StringProperty

> extends ValidatableProp if (maxLength != null && propValue.length()>maxLength) { result.add(propertyName + "' length is expected to be at max " + maxLength + " but length of " + display(propValue) + " is " + propValue.length()); } - if (matchesRegEx != null && !matchesRegEx.matcher(propValue).matches()) { - result.add(propertyName + "' is expected to be match " + matchesRegEx + " but " + display(propValue) + " does not match"); + if (matchesRegEx != null && + stream(matchesRegEx).map(p -> p.matcher(propValue)).noneMatch(Matcher::matches)) { + result.add(propertyName + "' is expected to match any of " + Arrays.toString(matchesRegEx) + " but " + display(propValue) + " does not match any"); } if (isReadOnly() && propValue != null) { result.add(propertyName + "' is readonly but given as " + display(propValue)); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java index 76fc451e..346ee08b 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java @@ -1,15 +1,16 @@ package net.hostsharing.hsadminng.hs.validation; import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.experimental.Accessors; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; +import lombok.experimental.Accessors; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; import net.hostsharing.hsadminng.mapper.Array; import org.apache.commons.lang3.function.TriFunction; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; @@ -22,6 +23,7 @@ import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; import static java.util.Collections.emptyList; import static java.util.Optional.ofNullable; +import static org.apache.commons.lang3.ObjectUtils.isArray; @Getter @RequiredArgsConstructor @@ -29,6 +31,7 @@ public abstract class ValidatableProperty

, T protected static final String[] KEY_ORDER_HEAD = Array.of("propertyName"); protected static final String[] KEY_ORDER_TAIL = Array.of("required", "defaultValue", "readOnly", "writeOnly", "computed", "isTotalsValidator", "thresholdPercentage"); + protected static final String[] KEY_ORDER = Array.join(KEY_ORDER_HEAD, KEY_ORDER_TAIL); final Class type; final String propertyName; @@ -238,8 +241,8 @@ protected void setDeferredInit(final Function[], T[]> } private Object arrayToList(final Object value) { - if ( value instanceof String[]) { - return List.of((String[])value); + if (isArray(value)) { + return Arrays.stream((Object[])value).map(Object::toString).toList(); } return value; } @@ -264,4 +267,9 @@ protected void setDeferredInit(final Function[], T[]> public T compute(final E entity) { return computedBy.apply(entity); } + + @Override + public String toString() { + return toOrderedMap().toString(); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/mapper/Array.java b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java index 57e76381..80970aa4 100644 --- a/src/main/java/net/hostsharing/hsadminng/mapper/Array.java +++ b/src/main/java/net/hostsharing/hsadminng/mapper/Array.java @@ -51,13 +51,16 @@ public class Array { return of(); } - public static T[] insertAfterEntry(final T[] array, final T entryToFind, final T newEntry) { + @SafeVarargs + public static T[] insertNewEntriesAfterExistingEntry(final T[] array, final T entryToFind, final T... newEntries) { final var arrayList = new ArrayList<>(asList(array)); final var index = arrayList.indexOf(entryToFind); if (index < 0) { throw new IllegalArgumentException("entry "+ entryToFind + " not found in " + Arrays.toString(array)); } - arrayList.add(index + 1, newEntry); + for (int n = 0; n < newEntries.length; ++n) { + arrayList.add(index +n + 1, newEntries[n]); + } @SuppressWarnings("unchecked") final var extendedArray = (T[]) java.lang.reflect.Array.newInstance(array.getClass().getComponentType(), array.length); 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 c82bd768..32f2804a 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 @@ -73,6 +73,7 @@ begin values (managedServerUuid, relatedManagedServerBookingItem.uuid, 'MANAGED_SERVER', null, null, 'vm10' || debitorNumberSuffix, 'some ManagedServer', '{ "monit_max_cpu_usage": 90, "monit_max_ram_usage": 80, "monit_max_ssd_usage": 70 }'::jsonb), (uuid_generate_v4(), relatedCloudServerBookingItem.uuid, 'CLOUD_SERVER', null, null, 'vm20' || debitorNumberSuffix, 'another CloudServer', '{}'::jsonb), (managedWebspaceUuid, relatedManagedWebspaceBookingItem.uuid, 'MANAGED_WEBSPACE', managedServerUuid, null, defaultPrefix || '01', 'some Webspace', '{}'::jsonb), + (uuid_generate_v4(), null, 'EMAIL_ALIAS', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some E-Mail-Alias', '{ "target": [ "office@example.org", "archive@example.com" ] }'::jsonb), (webUnixUserUuid, null, 'UNIX_USER', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some UnixUser for Website', '{ "SSD-soft-quota": "128", "SSD-hard-quota": "256", "HDD-soft-quota": "512", "HDD-hard-quota": "1024"}'::jsonb), (uuid_generate_v4(), null, 'DOMAIN_HTTP_SETUP', managedWebspaceUuid, webUnixUserUuid, defaultPrefix || '.example.org', 'some Domain-HTTP-Setup', '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*"}'::jsonb); end; $$; 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 89de41bc..20ebd989 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 @@ -29,6 +29,7 @@ import java.util.UUID; import java.util.function.Supplier; import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.EMAIL_ALIAS; 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.hs.hosting.asset.HsHostingAssetType.UNIX_USER; @@ -101,7 +102,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Test - void globalAdmin_canViewAllAssetsByType() { + void webspaceAgent_canViewAllAssetsByType() { // given context("superuser-alex@hostsharing.net"); @@ -109,42 +110,25 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup RestAssured // @formatter:off .given() .header("current-user", "superuser-alex@hostsharing.net") + .header("assumed-roles", "hs_hosting_asset#fir01:AGENT") .port(port) .when() - . get("http://localhost/api/hs/hosting/assets?type=" + MANAGED_SERVER) + . get("http://localhost/api/hs/hosting/assets?type=" + EMAIL_ALIAS) .then().log().all().assertThat() .statusCode(200) .contentType("application/json") .body("", lenientlyEquals(""" [ { - "type": "MANAGED_SERVER", - "identifier": "vm1011", - "caption": "some ManagedServer", + "type": "EMAIL_ALIAS", + "identifier": "fir01-web", + "caption": "some E-Mail-Alias", + "alarmContact": null, "config": { - "monit_max_cpu_usage": 90, - "monit_max_ram_usage": 80, - "monit_max_ssd_usage": 70 - } - }, - { - "type": "MANAGED_SERVER", - "identifier": "vm1012", - "caption": "some ManagedServer", - "config": { - "monit_max_cpu_usage": 90, - "monit_max_ram_usage": 80, - "monit_max_ssd_usage": 70 - } - }, - { - "type": "MANAGED_SERVER", - "identifier": "vm1013", - "caption": "some ManagedServer", - "config": { - "monit_max_cpu_usage": 90, - "monit_max_ram_usage": 80, - "monit_max_ssd_usage": 70 + "target": [ + "office@example.org", + "archive@example.com" + ] } } ] diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java new file mode 100644 index 00000000..529d34cd --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java @@ -0,0 +1,243 @@ +package net.hostsharing.hsadminng.hs.hosting.asset; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; +import net.hostsharing.hsadminng.mapper.Array; +import net.hostsharing.hsadminng.mapper.Mapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.SynchronizationType; +import java.util.List; +import java.util.Map; + +import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_CLOUD_SERVER_BOOKING_ITEM; +import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_SERVER_BOOKING_ITEM; +import static net.hostsharing.hsadminng.hs.hosting.asset.TestHsHostingAssetEntities.TEST_MANAGED_WEBSPACE_HOSTING_ASSET; +import static net.hostsharing.hsadminng.hs.office.contact.TestHsOfficeContact.TEST_CONTACT; +import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(HsHostingAssetController.class) +@Import(Mapper.class) +@RunWith(SpringRunner.class) +public class HsHostingAssetControllerRestTest { + + @Autowired + MockMvc mockMvc; + + @MockBean + Context contextMock; + + @Autowired + Mapper mapper; + + @Mock + private EntityManager em; + + @MockBean + EntityManagerFactory emf; + + @MockBean + @SuppressWarnings("unused") // bean needs to be present for HsHostingAssetController + private HsBookingItemRepository bookingItemRepo; + + @MockBean + private HsHostingAssetRepository hostingAssetRepo; + + enum ListTestCases { + CLOUD_SERVER( + List.of( + HsHostingAssetEntity.builder() + .type(HsHostingAssetType.CLOUD_SERVER) + .bookingItem(TEST_CLOUD_SERVER_BOOKING_ITEM) + .identifier("vm1234") + .caption("some fake cloud-server") + .alarmContact(TEST_CONTACT) + .build()), + """ + [ + { + "type": "CLOUD_SERVER", + "identifier": "vm1234", + "caption": "some fake cloud-server", + "alarmContact": { + "caption": "some contact", + "postalAddress": "address of some contact", + "emailAddresses": { + "main": "some-contact@example.com" + } + }, + "config": {} + } + ] + """), + MANAGED_SERVER( + List.of( + HsHostingAssetEntity.builder() + .type(HsHostingAssetType.MANAGED_SERVER) + .bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM) + .identifier("vm1234") + .caption("some fake managed-server") + .alarmContact(TEST_CONTACT) + .config(Map.ofEntries( + entry("monit_max_ssd_usage", 70), + entry("monit_max_cpu_usage", 80), + entry("monit_max_ram_usage", 90) + )) + .build()), + """ + [ + { + "type": "MANAGED_SERVER", + "identifier": "vm1234", + "caption": "some fake managed-server", + "alarmContact": { + "caption": "some contact", + "postalAddress": "address of some contact", + "emailAddresses": { + "main": "some-contact@example.com" + } + }, + "config": { + "monit_max_ssd_usage": 70, + "monit_max_cpu_usage": 80, + "monit_max_ram_usage": 90 + } + } + ] + """), + UNIX_USER( + List.of( + HsHostingAssetEntity.builder() + .type(HsHostingAssetType.UNIX_USER) + .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) + .identifier("xyz00-office") + .caption("some fake Unix-User") + .config(Map.ofEntries( + entry("password", "$6$salt$hashed-salted-password"), + entry("totpKey", "0x0123456789abcdef"), + entry("shell", "/bin/bash"), + entry("SSD-soft-quota", 128), + entry("SSD-hard-quota", 256), + entry("HDD-soft-quota", 256), + entry("HDD-hard-quota", 512))) + .build()), + """ + [ + { + "type": "UNIX_USER", + "identifier": "xyz00-office", + "caption": "some fake Unix-User", + "alarmContact": null, + "config": { + "SSD-soft-quota": 128, + "SSD-hard-quota": 256, + "HDD-soft-quota": 256, + "HDD-hard-quota": 512, + "shell": "/bin/bash", + "homedir": "/home/pacs/xyz00/users/office" + } + } + ] + """), + EMAIL_ALIAS( + List.of( + HsHostingAssetEntity.builder() + .type(HsHostingAssetType.EMAIL_ALIAS) + .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) + .identifier("xyz00-office") + .caption("some fake EMail-Alias") + .config(Map.ofEntries( + entry("target", Array.of("xyz00", "xyz00-abc", "office@example.com")) + )) + .build()), + """ + [ + { + "type": "EMAIL_ALIAS", + "identifier": "xyz00-office", + "caption": "some fake EMail-Alias", + "alarmContact": null, + "config": { + "target": ["xyz00","xyz00-abc","office@example.com"] + } + } + ] + """); + + final HsHostingAssetType assetType; + final List givenHostingAssetsOfType; + final String expectedResponse; + final JsonNode expectedResponseJson; + + @SneakyThrows + ListTestCases( + final List givenHostingAssetsOfType, + final String expectedResponse) { + this.assetType = HsHostingAssetType.valueOf(name()); + this.givenHostingAssetsOfType = givenHostingAssetsOfType; + this.expectedResponse = expectedResponse; + this.expectedResponseJson = new ObjectMapper().readTree(expectedResponse); + } + + @SneakyThrows + JsonNode expectedConfig(final int n) { + return expectedResponseJson.get(n).path("config"); + } + } + + @BeforeEach + void init() { + when(emf.createEntityManager()).thenReturn(em); + when(emf.createEntityManager(any(Map.class))).thenReturn(em); + when(emf.createEntityManager(any(SynchronizationType.class))).thenReturn(em); + when(emf.createEntityManager(any(SynchronizationType.class), any(Map.class))).thenReturn(em); + } + + @ParameterizedTest + @EnumSource(HsHostingAssetControllerRestTest.ListTestCases.class) + void shouldListAssets(final HsHostingAssetControllerRestTest.ListTestCases testCase) throws Exception { + // given + when(hostingAssetRepo.findAllByCriteria(null, null, testCase.assetType)) + .thenReturn(testCase.givenHostingAssetsOfType); + + // when + final var result = mockMvc.perform(MockMvcRequestBuilders + .get("/api/hs/hosting/assets?type="+testCase.name()) + .header("current-user", "superuser-alex@hostsharing.net") + .accept(MediaType.APPLICATION_JSON)) + + // then + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$", lenientlyEquals(testCase.expectedResponse))) + .andReturn(); + + // and the config properties do match not just leniently but even strictly + final var resultBody = new ObjectMapper().readTree(result.getResponse().getContentAsString()); + for (int n = 0; n < resultBody.size(); ++n) { + assertThat(resultBody.get(n).path("config")).isEqualTo(testCase.expectedConfig(n)); + } + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java index 7910408c..e8323839 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java @@ -34,7 +34,8 @@ class HsHostingAssetPropsControllerAcceptanceTest { "MANAGED_SERVER", "MANAGED_WEBSPACE", "CLOUD_SERVER", - "UNIX_USER" + "UNIX_USER", + "EMAIL_ALIAS" ] """)); // @formatter:on diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/TestHsHostingAssetEntities.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/TestHsHostingAssetEntities.java new file mode 100644 index 00000000..e409306b --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/TestHsHostingAssetEntities.java @@ -0,0 +1,22 @@ +package net.hostsharing.hsadminng.hs.hosting.asset; + +import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_SERVER_BOOKING_ITEM; +import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_WEBSPACE_BOOKING_ITEM; + +public class TestHsHostingAssetEntities { + + public static final HsHostingAssetEntity TEST_MANAGED_SERVER_HOSTING_ASSET = HsHostingAssetEntity.builder() + .type(HsHostingAssetType.MANAGED_SERVER) + .identifier("vm1234") + .caption("some managed server") + .bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM) + .build(); + + public static final HsHostingAssetEntity TEST_MANAGED_WEBSPACE_HOSTING_ASSET = HsHostingAssetEntity.builder() + .type(HsHostingAssetType.MANAGED_WEBSPACE) + .identifier("xyz00") + .caption("some managed webspace") + .bookingItem(TEST_MANAGED_WEBSPACE_BOOKING_ITEM) + .build(); + +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java new file mode 100644 index 00000000..6c35078b --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java @@ -0,0 +1,114 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.mapper.Array; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_SERVER_BOOKING_ITEM; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.EMAIL_ALIAS; +import static net.hostsharing.hsadminng.hs.hosting.asset.TestHsHostingAssetEntities.TEST_MANAGED_SERVER_HOSTING_ASSET; +import static net.hostsharing.hsadminng.hs.hosting.asset.TestHsHostingAssetEntities.TEST_MANAGED_WEBSPACE_HOSTING_ASSET; +import static org.assertj.core.api.Assertions.assertThat; + +class HsEMailAliasHostingAssetValidatorUnitTest { + + @Test + void containsAllValidations() { + // when + final var validator = HsHostingAssetEntityValidatorRegistry.forType(EMAIL_ALIAS); + + // then + assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( + "{type=string[], propertyName=target, elementsOf={type=string, propertyName=target, matchesRegEx=[^[a-z][a-z0-9]{2}[0-9]{2}(-[a-z0-9]+)?$, ^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$], maxLength=320}, required=true, minLength=1}"); + } + + @Test + void validatesValidEntity() { + // given + final var emailAliasHostingAssetEntity = HsHostingAssetEntity.builder() + .type(EMAIL_ALIAS) + .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) + .identifier("xyz00-office") + .config(Map.ofEntries( + entry("target", Array.of("xyz00", "xyz00-abc", "office@example.com")) + )) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(emailAliasHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(emailAliasHostingAssetEntity); + + // then + assertThat(result).isEmpty(); + } + + @Test + void validatesProperties() { + // given + final var emailAliasHostingAssetEntity = HsHostingAssetEntity.builder() + .type(EMAIL_ALIAS) + .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) + .identifier("xyz00-office") + .config(Map.ofEntries( + entry("target", Array.of("xyz00", "xyz00-abc", "garbage", "office@example.com")) + )) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(emailAliasHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(emailAliasHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'EMAIL_ALIAS:xyz00-office.config.target' is expected to match any of [^[a-z][a-z0-9]{2}[0-9]{2}(-[a-z0-9]+)?$, ^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$] but 'garbage' does not match any"); + } + + @Test + void validatesInvalidIdentifier() { + // given + final var emailAliasHostingAssetEntity = HsHostingAssetEntity.builder() + .type(EMAIL_ALIAS) + .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) + .identifier("abc00-office") + .config(Map.ofEntries( + entry("target", Array.of("office@example.com")) + )) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(emailAliasHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(emailAliasHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'identifier' expected to match '^xyz00$|^xyz00-[a-z0-9]+$', but is 'abc00-office'"); + } + + @Test + void validatesInvalidReferences() { + // given + final var emailAliasHostingAssetEntity = HsHostingAssetEntity.builder() + .type(EMAIL_ALIAS) + .bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM) + .parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET) + .assignedToAsset(TEST_MANAGED_SERVER_HOSTING_ASSET) + .identifier("abc00-office") + .config(Map.ofEntries( + entry("target", Array.of("office@example.com")) + )) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(emailAliasHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(emailAliasHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'EMAIL_ALIAS:abc00-office.bookingItem' must be null but is set to D-1234500:test project:test project booking item", + "'EMAIL_ALIAS:abc00-office.parentAsset' must be of type MANAGED_WEBSPACE but is of type MANAGED_SERVER", + "'EMAIL_ALIAS:abc00-office.assignedToAsset' must be null but is set to D-1234500:test project:test project booking item"); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java index 881b5c5f..b24a035c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java @@ -32,7 +32,8 @@ class HsHostingAssetEntityValidatorRegistryUnitTest { HsHostingAssetType.CLOUD_SERVER, HsHostingAssetType.MANAGED_SERVER, HsHostingAssetType.MANAGED_WEBSPACE, - HsHostingAssetType.UNIX_USER + HsHostingAssetType.UNIX_USER, + HsHostingAssetType.EMAIL_ALIAS ); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java index 5ef61da9..ce1b5a1d 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java @@ -110,7 +110,7 @@ class HsUnixUserHostingAssetValidatorUnitTest { "'UNIX_USER:abc00-temp.config.HDD soft quota' is expected to be at most 100 but is 200", "'UNIX_USER:abc00-temp.config.shell' is expected to be one of [/bin/false, /bin/bash, /bin/csh, /bin/dash, /usr/bin/tcsh, /usr/bin/zsh, /usr/bin/passwd] but is '/is/invalid'", "'UNIX_USER:abc00-temp.config.homedir' is readonly but given as '/is/read-only'", - "'UNIX_USER:abc00-temp.config.totpKey' is expected to be match ^0x([0-9A-Fa-f]{2})+$ but provided value does not match", + "'UNIX_USER:abc00-temp.config.totpKey' is expected to match any of [^0x([0-9A-Fa-f]{2})+$] but provided value does not match any", "'UNIX_USER:abc00-temp.config.password' length is expected to be at min 8 but length of provided value is 5", "'UNIX_USER:abc00-temp.config.password' must contain at least one character of at least 3 of the following groups: upper case letters, lower case letters, digits, special characters" ); @@ -168,7 +168,7 @@ class HsUnixUserHostingAssetValidatorUnitTest { "{type=integer, propertyName=HDD soft quota, unit=GB, maxFrom=HDD hard quota}", "{type=enumeration, propertyName=shell, values=[/bin/false, /bin/bash, /bin/csh, /bin/dash, /usr/bin/tcsh, /usr/bin/zsh, /usr/bin/passwd], defaultValue=/bin/false}", "{type=string, propertyName=homedir, readOnly=true, computed=true}", - "{type=string, propertyName=totpKey, matchesRegEx=^0x([0-9A-Fa-f]{2})+$, minLength=20, maxLength=256, writeOnly=true, undisclosed=true}", + "{type=string, propertyName=totpKey, matchesRegEx=[^0x([0-9A-Fa-f]{2})+$], minLength=20, maxLength=256, writeOnly=true, undisclosed=true}", "{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, computed=true, hashedUsing=SHA512, undisclosed=true}" ); } From f6d66d5712b7ffbe806c8e77f0b09e8e30862a61 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 5 Jul 2024 11:56:32 +0200 Subject: [PATCH 15/18] add-domain-setup-validation (#71) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/71 Reviewed-by: Marc Sandlus --- .../asset/HsHostingAssetController.java | 2 + .../hosting/asset/HsHostingAssetEntity.java | 10 + .../hs/hosting/asset/HsHostingAssetType.java | 7 +- ...HsDomainDnsSetupHostingAssetValidator.java | 106 ++++++++ .../HsDomainSetupHostingAssetValidator.java | 27 ++ .../HsHostingAssetEntityProcessor.java | 23 ++ ...HsHostingAssetEntityValidatorRegistry.java | 2 + .../hs/validation/HsEntityValidator.java | 31 +++ .../hsadminng/system/SystemProcess.java | 57 ++++ .../hs-hosting/hs-hosting-asset-schemas.yaml | 1 + .../7010-hs-hosting-asset.sql | 10 +- .../7013-hs-hosting-asset-rbac.md | 4 +- .../7013-hs-hosting-asset-rbac.sql | 115 +------- .../7018-hs-hosting-asset-test-data.sql | 6 +- .../HsHostingAssetControllerRestTest.java | 61 +++++ ...ingAssetPropsControllerAcceptanceTest.java | 4 +- ...HostingAssetRepositoryIntegrationTest.java | 36 ++- ...DnsSetupHostingAssetValidatorUnitTest.java | 245 ++++++++++++++++++ ...ainSetupHostingAssetValidatorUnitTest.java | 111 ++++++++ ...gAssetEntityValidatorRegistryUnitTest.java | 4 +- .../hsadminng/system/SystemProcessTest.java | 81 ++++++ 21 files changed, 821 insertions(+), 122 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/system/SystemProcess.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/system/SystemProcessTest.java 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 d9b6492f..6e082c05 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 @@ -73,6 +73,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { final var entity = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); final var mapped = new HsHostingAssetEntityProcessor(entity) + .preprocessEntity() .validateEntity() .prepareForSave() .saveUsing(assetRepo::save) @@ -133,6 +134,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { new HsHostingAssetEntityPatcher(em, entity).apply(body); final var mapped = new HsHostingAssetEntityProcessor(entity) + .preprocessEntity() .validateEntity() .prepareForSave() .saveUsing(assetRepo::save) 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 ae181921..80f9294c 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 @@ -41,6 +41,7 @@ import java.util.Map; import java.util.UUID; import static java.util.Collections.emptyMap; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef.inCaseOf; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL; @@ -51,6 +52,7 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.UPDATE; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.GUEST; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.OWNER; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.REFERRER; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.TENANT; @@ -199,6 +201,13 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Properti directlyFetchedByDependsOnColumn(), NULLABLE) + .switchOnColumn("type", + inCaseOf("DOMAIN_SETUP", then -> { + then.toRole(GLOBAL, GUEST).grantPermission(INSERT); + then.toRole(GLOBAL, ADMIN).grantPermission(SELECT); // TODO.spec: replace by a proper solution + }) + ) + .createRole(OWNER, (with) -> { with.incomingSuperRole("bookingItem", ADMIN); with.incomingSuperRole("parentAsset", ADMIN); @@ -219,6 +228,7 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Properti with.incomingSuperRole("alarmContact", ADMIN); with.permission(SELECT); }) + .limitDiagramTo("asset", "bookingItem", "bookingItem.debitorRel", "parentAsset", "assignedToAsset", "alarmContact", "global"); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java index f02a50f0..88ccca45 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java @@ -6,9 +6,10 @@ public enum HsHostingAssetType { MANAGED_SERVER, // named e.g. vm1234 MANAGED_WEBSPACE(MANAGED_SERVER), // named eg. xyz00 UNIX_USER(MANAGED_WEBSPACE), // named e.g. xyz00-abc - DOMAIN_DNS_SETUP(MANAGED_WEBSPACE), // named e.g. example.org - DOMAIN_HTTP_SETUP(MANAGED_WEBSPACE), // named e.g. example.org - DOMAIN_EMAIL_SETUP(MANAGED_WEBSPACE), // named e.g. example.org + DOMAIN_SETUP, // named e.g. example.org + DOMAIN_DNS_SETUP(DOMAIN_SETUP), // named e.g. example.org + DOMAIN_HTTP_SETUP(DOMAIN_SETUP), // named e.g. example.org + DOMAIN_EMAIL_SETUP(DOMAIN_SETUP), // named e.g. example.org // TODO.spec: SECURE_MX EMAIL_ALIAS(MANAGED_WEBSPACE), // named e.g. xyz00-abc diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java new file mode 100644 index 00000000..e09f77ef --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java @@ -0,0 +1,106 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import lombok.SneakyThrows; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; +import net.hostsharing.hsadminng.system.SystemProcess; + +import java.util.List; +import java.util.regex.Pattern; + +import static java.util.Arrays.stream; +import static java.util.Optional.ofNullable; +import static net.hostsharing.hsadminng.hs.validation.ArrayProperty.arrayOf; +import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty; +import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; +import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; + +class HsDomainDnsSetupHostingAssetValidator extends HsHostingAssetEntityValidator { + + // according to RFC 1035 (section 5) and RFC 1034 + static final String RR_REGEX_NAME = "([a-z0-9\\.-]+|@)\\s+"; + static final String RR_REGEX_TTL = "(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*"; + static final String RR_REGEX_IN = "IN\\s+"; // record class IN for Internet + static final String RR_RECORD_TYPE = "[A-Z]+\\s+"; + static final String RR_RECORD_DATA = "[^;].*"; + static final String RR_COMMENT = "(;.*)*"; + + static final String RR_REGEX_TTL_IN = + RR_REGEX_NAME + RR_REGEX_TTL + RR_REGEX_IN + RR_RECORD_TYPE + RR_RECORD_DATA + RR_COMMENT; + + static final String RR_REGEX_IN_TTL = + RR_REGEX_NAME + RR_REGEX_IN + RR_REGEX_TTL + RR_RECORD_TYPE + RR_RECORD_DATA + RR_COMMENT; + + HsDomainDnsSetupHostingAssetValidator() { + super( BookingItem.mustBeNull(), + ParentAsset.mustBeOfType(HsHostingAssetType.DOMAIN_SETUP), + AssignedToAsset.mustBeNull(), + AlarmContact.isOptional(), + + integerProperty("TTL").min(0).withDefault(21600), + booleanProperty("auto-SOA-RR").withDefault(true), + booleanProperty("auto-NS-RR").withDefault(true), + booleanProperty("auto-MX-RR").withDefault(true), + booleanProperty("auto-A-RR").withDefault(true), + booleanProperty("auto-AAAA-RR").withDefault(true), + booleanProperty("auto-MAILSERVICES-RR").withDefault(true), + booleanProperty("auto-AUTOCONFIG-RR").withDefault(true), // TODO.spec: does that already exist? + booleanProperty("auto-AUTODISCOVER-RR").withDefault(true), + booleanProperty("auto-DKIM-RR").withDefault(true), + booleanProperty("auto-SPF-RR").withDefault(true), + booleanProperty("auto-WILDCARD-MX-RR").withDefault(true), + booleanProperty("auto-WILDCARD-A-RR").withDefault(true), + booleanProperty("auto-WILDCARD-AAAA-RR").withDefault(true), + booleanProperty("auto-WILDCARD-DKIM-RR").withDefault(true), // TODO.spec: check, if that really works + booleanProperty("auto-WILDCARD-SPF-RR").withDefault(true), + arrayOf( + stringProperty("user-RR").matchesRegEx(RR_REGEX_TTL_IN, RR_REGEX_IN_TTL).required() + ).optional()); + } + + @Override + protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { + return Pattern.compile("^" + assetEntity.getParentAsset().getIdentifier() + "$"); + } + + @Override + public void preprocessEntity(final HsHostingAssetEntity entity) { + super.preprocessEntity(entity); + if (entity.getIdentifier() == null) { + ofNullable(entity.getParentAsset()).ifPresent(pa -> entity.setIdentifier(pa.getIdentifier())); + } + } + + @Override + @SneakyThrows + public List validateContext(final HsHostingAssetEntity assetEntity) { + final var result = super.validateContext(assetEntity); + + // TODO.spec: define which checks should get raised to error level + final var namedCheckZone = new SystemProcess("named-checkzone", assetEntity.getIdentifier()); + if (namedCheckZone.execute(toZonefileString(assetEntity)) != 0) { + // yes, named-checkzone writes error messages to stdout + stream(namedCheckZone.getStdOut().split("\n")) + .map(line -> line.replaceAll(" stream-0x[0-9a-f:]+", "")) + .forEach(result::add); + } + return result; + } + + String toZonefileString(final HsHostingAssetEntity assetEntity) { + // TODO.spec: we need to expand the templates (auto-...) in the same way as in Saltstack + return """ + $ORIGIN {domain}. + $TTL {ttl} + + ; these records are just placeholders to create a valid zonefile for the validation + @ 1814400 IN SOA {domain}. root.{domain} ( 1999010100 10800 900 604800 86400 ) + @ IN NS ns + + {userRRs} + """ + .replace("{domain}", assetEntity.getIdentifier()) + .replace("{ttl}", getPropertyValue(assetEntity, "TTL")) + .replace("{userRRs}", getPropertyValues(assetEntity, "user-RR") ); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java new file mode 100644 index 00000000..d2693f7e --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java @@ -0,0 +1,27 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; + +import java.util.regex.Pattern; + +class HsDomainSetupHostingAssetValidator extends HsHostingAssetEntityValidator { + + public static final String DOMAIN_NAME_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(? validator; + private String expectedStep = "preprocessEntity"; private HsHostingAssetEntity entity; private HsHostingAssetResource resource; @@ -22,8 +23,16 @@ public class HsHostingAssetEntityProcessor { this.validator = HsHostingAssetEntityValidatorRegistry.forType(entity.getType()); } + /// initial step allowing to set default values before any validations + public HsHostingAssetEntityProcessor preprocessEntity() { + step("preprocessEntity", "validateEntity"); + validator.preprocessEntity(entity); + return this; + } + /// validates the entity itself including its properties public HsHostingAssetEntityProcessor validateEntity() { + step("validateEntity", "prepareForSave"); MultiValidationException.throwIfNotEmpty(validator.validateEntity(entity)); return this; } @@ -31,17 +40,20 @@ public class HsHostingAssetEntityProcessor { /// hashing passwords etc. @SuppressWarnings("unchecked") public HsHostingAssetEntityProcessor prepareForSave() { + step("prepareForSave", "saveUsing"); validator.prepareProperties(entity); return this; } public HsHostingAssetEntityProcessor saveUsing(final Function saveFunction) { + step("saveUsing", "validateContext"); entity = saveFunction.apply(entity); return this; } /// validates the entity within it's parent and child hierarchy (e.g. totals validators and other limits) public HsHostingAssetEntityProcessor validateContext() { + step("validateContext", "mapUsing"); MultiValidationException.throwIfNotEmpty(validator.validateContext(entity)); return this; } @@ -49,6 +61,7 @@ public class HsHostingAssetEntityProcessor { /// maps entity to JSON resource representation public HsHostingAssetEntityProcessor mapUsing( final Function mapFunction) { + step("mapUsing", "revampProperties"); resource = mapFunction.apply(entity); return this; } @@ -56,8 +69,18 @@ public class HsHostingAssetEntityProcessor { /// removes write-only-properties and ads computed-properties @SuppressWarnings("unchecked") public HsHostingAssetResource revampProperties() { + step("revampProperties", null); final var revampedProps = validator.revampProperties(entity, (Map) resource.getConfig()); resource.setConfig(revampedProps); return resource; } + + // Makes sure that the steps are called in the correct order. + // Could also be implemented using an interface per method, but that seems exaggerated. + private void step(final String current, final String next) { + if (!expectedStep.equals(current)) { + throw new IllegalStateException("expected " + expectedStep + " but got " + current); + } + expectedStep = next; + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java index a30108e7..3ae14256 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java @@ -20,6 +20,8 @@ public class HsHostingAssetEntityValidatorRegistry { register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator()); register(UNIX_USER, new HsUnixUserHostingAssetValidator()); register(EMAIL_ALIAS, new HsEMailAliasHostingAssetValidator()); + register(DOMAIN_SETUP, new HsDomainSetupHostingAssetValidator()); + register(DOMAIN_DNS_SETUP, new HsDomainDnsSetupHostingAssetValidator()); } private static void register(final Enum type, final HsEntityValidator validator) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java index 13cb3f05..de4b70bc 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java @@ -6,7 +6,9 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.Supplier; +import java.util.stream.Collectors; import static java.util.Arrays.stream; import static java.util.Collections.emptyList; @@ -41,6 +43,19 @@ public abstract class HsEntityValidator { .toList(); } + public final Map> propertiesMap() { + return Arrays.stream(propertyValidators) + .map(ValidatableProperty::toOrderedMap) + .collect(Collectors.toMap(p -> p.get("propertyName").toString(), p -> p)); + } + + /** + Gets called before any validations take place. + Allows to initialize fields and properties to default values. + */ + public void preprocessEntity(final E entity) { + } + protected ArrayList validateProperties(final PropertiesProvider propsProvider) { final var result = new ArrayList(); @@ -109,4 +124,20 @@ public abstract class HsEntityValidator { }); return copy; } + + protected String getPropertyValue(final PropertiesProvider entity, final String propertyName) { + final var rawValue = entity.getDirectValue(propertyName, Object.class); + if (rawValue != null) { + return rawValue.toString(); + } + return Objects.toString(propertiesMap().get(propertyName).get("defaultValue")); + } + + protected String getPropertyValues(final PropertiesProvider entity, final String propertyName) { + final var rawValue = entity.getDirectValue(propertyName, Object[].class); + if (rawValue != null) { + return stream(rawValue).map(Object::toString).collect(Collectors.joining("\n")); + } + return ""; + } } diff --git a/src/main/java/net/hostsharing/hsadminng/system/SystemProcess.java b/src/main/java/net/hostsharing/hsadminng/system/SystemProcess.java new file mode 100644 index 00000000..149c6019 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/system/SystemProcess.java @@ -0,0 +1,57 @@ +package net.hostsharing.hsadminng.system; + +import lombok.Getter; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; + +public class SystemProcess { + private final ProcessBuilder processBuilder; + + @Getter + private String stdOut; + @Getter + private String stdErr; + + public SystemProcess(final String... command) { + this.processBuilder = new ProcessBuilder(command); + } + + public int execute() throws IOException, InterruptedException { + final var process = processBuilder.start(); + stdOut = fetchOutput(process.getInputStream()); // yeah, twisted ProcessBuilder API + stdErr = fetchOutput(process.getErrorStream()); + return process.waitFor(); + } + + public int execute(final String input) throws IOException, InterruptedException { + final var process = processBuilder.start(); + feedInput(input, process); + stdOut = fetchOutput(process.getInputStream()); // yeah, twisted ProcessBuilder API + stdErr = fetchOutput(process.getErrorStream()); + return process.waitFor(); + } + + private static void feedInput(final String input, final Process process) throws IOException { + try ( + final OutputStreamWriter stdIn = new OutputStreamWriter(process.getOutputStream()); // yeah, twisted ProcessBuilder API + final BufferedWriter writer = new BufferedWriter(stdIn)) { + writer.write(input); + writer.flush(); + } + } + + private static String fetchOutput(final InputStream inputStream) throws IOException { + final var output = new StringBuilder(); + try (final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + for (String line; (line = reader.readLine()) != null; ) { + output.append(line).append(System.lineSeparator()); + } + } + return output.toString(); + } +} diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml index 934c9647..a9ab7f64 100644 --- a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml @@ -10,6 +10,7 @@ components: - MANAGED_SERVER - MANAGED_WEBSPACE - UNIX_USER + - DOMAIN_SETUP - DOMAIN_DNS_SETUP - DOMAIN_HTTP_SETUP - DOMAIN_EMAIL_SETUP 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 bd6ff6e4..eb335238 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 @@ -9,6 +9,7 @@ create type HsHostingAssetType as enum ( 'MANAGED_SERVER', 'MANAGED_WEBSPACE', 'UNIX_USER', + 'DOMAIN_SETUP', 'DOMAIN_DNS_SETUP', 'DOMAIN_HTTP_SETUP', 'DOMAIN_EMAIL_SETUP', @@ -36,7 +37,7 @@ create table if not exists hs_hosting_asset alarmContactUuid uuid null references hs_office_contact(uuid) initially deferred, constraint chk_hs_hosting_asset_has_booking_item_or_parent_asset - check (bookingItemUuid is not null or parentAssetUuid is not null) + check (bookingItemUuid is not null or parentAssetUuid is not null or type='DOMAIN_SETUP') ); --// @@ -63,9 +64,10 @@ begin when 'MANAGED_SERVER' then null when 'MANAGED_WEBSPACE' then 'MANAGED_SERVER' when 'UNIX_USER' then 'MANAGED_WEBSPACE' - when 'DOMAIN_DNS_SETUP' then 'MANAGED_WEBSPACE' - when 'DOMAIN_HTTP_SETUP' then 'MANAGED_WEBSPACE' - when 'DOMAIN_EMAIL_SETUP' then 'MANAGED_WEBSPACE' + when 'DOMAIN_SETUP' then null + when 'DOMAIN_DNS_SETUP' then 'DOMAIN_SETUP' + when 'DOMAIN_HTTP_SETUP' then 'DOMAIN_SETUP' + when 'DOMAIN_EMAIL_SETUP' then 'DOMAIN_SETUP' when 'EMAIL_ALIAS' then 'MANAGED_WEBSPACE' when 'EMAIL_ADDRESS' then 'DOMAIN_EMAIL_SETUP' when 'PGSQL_USER' then 'MANAGED_WEBSPACE' 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 f0b250db..37b47e15 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 @@ -36,9 +36,9 @@ subgraph asset["`**asset**`"] style asset:permissions fill:#dd4901,stroke:white perm:asset:INSERT{{asset:INSERT}} + perm:asset:SELECT{{asset:SELECT}} perm:asset:DELETE{{asset:DELETE}} perm:asset:UPDATE{{asset:UPDATE}} - perm:asset:SELECT{{asset:SELECT}} end end @@ -103,6 +103,8 @@ role:alarmContact:ADMIN ==> role:asset:TENANT %% granting permissions to roles role:global:ADMIN ==> perm:asset:INSERT role:parentAsset:ADMIN ==> perm:asset:INSERT +role:global:GUEST ==> perm:asset:INSERT +role:global:ADMIN ==> perm:asset:SELECT 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 cbaffa47..5b740226 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 @@ -82,6 +82,13 @@ begin hsHostingAssetTENANT(newParentAsset)] ); + IF NEW.type = 'DOMAIN_SETUP' THEN + END IF; + + + + call grantPermissionToRole(createPermission(NEW.uuid, 'SELECT'), globalAdmin()); + call leaveTriggerForObjectUuid(NEW.uuid); end; $$; @@ -147,114 +154,6 @@ execute procedure updateTriggerForHsHostingAsset_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_hosting_asset ---------------------------- - --- 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_hosting_asset INSERT permission to specified role of new hs_hosting_asset rows. -*/ -create or replace function new_hs_hosting_asset_grants_insert_to_hs_hosting_asset_tf() - returns trigger - language plpgsql - strict as $$ -begin - -- unconditional for all rows in that table - call grantPermissionToRole( - createPermission(NEW.uuid, 'INSERT', 'hs_hosting_asset'), - hsHostingAssetADMIN(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_hosting_asset_grants_insert_to_hs_hosting_asset_tg - after insert on hs_hosting_asset - for each row -execute procedure new_hs_hosting_asset_grants_insert_to_hs_hosting_asset_tf(); - - --- ============================================================================ ---changeset hs_hosting_asset-rbac-CHECKING-INSERT-PERMISSION:1 endDelimiter:--// --- ---------------------------------------------------------------------------- - -/** - Checks if the user respectively the assumed roles are allowed to insert a row to hs_hosting_asset. -*/ -create or replace function hs_hosting_asset_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.parentAssetUuid - if hasInsertPermission(NEW.parentAssetUuid, 'hs_hosting_asset') then - return NEW; - end if; - - 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 - before insert on hs_hosting_asset - for each row - execute procedure hs_hosting_asset_insert_permission_check_tf(); ---// - - -- ============================================================================ --changeset hs-hosting-asset-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- 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 32f2804a..736c129d 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 @@ -23,6 +23,7 @@ declare managedServerUuid uuid; managedWebspaceUuid uuid; webUnixUserUuid uuid; + domainSetupUuid uuid; begin currentTask := 'creating hosting-asset test-data ' || givenProjectCaption; call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN'); @@ -65,6 +66,7 @@ begin select uuid_generate_v4() into managedServerUuid; select uuid_generate_v4() into managedWebspaceUuid; select uuid_generate_v4() into webUnixUserUuid; + select uuid_generate_v4() into domainSetupUuid; debitorNumberSuffix := relatedDebitor.debitorNumberSuffix; defaultPrefix := relatedDebitor.defaultPrefix; @@ -75,7 +77,9 @@ begin (managedWebspaceUuid, relatedManagedWebspaceBookingItem.uuid, 'MANAGED_WEBSPACE', managedServerUuid, null, defaultPrefix || '01', 'some Webspace', '{}'::jsonb), (uuid_generate_v4(), null, 'EMAIL_ALIAS', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some E-Mail-Alias', '{ "target": [ "office@example.org", "archive@example.com" ] }'::jsonb), (webUnixUserUuid, null, 'UNIX_USER', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some UnixUser for Website', '{ "SSD-soft-quota": "128", "SSD-hard-quota": "256", "HDD-soft-quota": "512", "HDD-hard-quota": "1024"}'::jsonb), - (uuid_generate_v4(), null, 'DOMAIN_HTTP_SETUP', managedWebspaceUuid, webUnixUserUuid, defaultPrefix || '.example.org', 'some Domain-HTTP-Setup', '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*"}'::jsonb); + (domainSetupUuid, null, 'DOMAIN_SETUP', null, null, defaultPrefix || '.example.org', 'some Domain-Setup', '{}'::jsonb), + (uuid_generate_v4(), null, 'DOMAIN_DNS_SETUP', domainSetupUuid, null, defaultPrefix || '.example.org', 'some Domain-DNS-Setup', '{}'::jsonb), + (uuid_generate_v4(), null, 'DOMAIN_HTTP_SETUP', domainSetupUuid, webUnixUserUuid, defaultPrefix || '.example.org', 'some Domain-HTTP-Setup', '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*"}'::jsonb); end; $$; --// diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java index 529d34cd..eed85585 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java @@ -185,6 +185,67 @@ public class HsHostingAssetControllerRestTest { } } ] + """), + DOMAIN_SETUP( + List.of( + HsHostingAssetEntity.builder() + .type(HsHostingAssetType.DOMAIN_SETUP) + .identifier("example.org") + .caption("some fake Domain-Setup") + .build()), + """ + [ + { + "type": "DOMAIN_SETUP", + "identifier": "example.org", + "caption": "some fake Domain-Setup", + "alarmContact": null, + "config": {} + } + ] + """), + DOMAIN_DNS_SETUP( + List.of( + HsHostingAssetEntity.builder() + .type(HsHostingAssetType.DOMAIN_DNS_SETUP) + .identifier("example.org") + .caption("some fake Domain-DNS-Setup") + .config(Map.ofEntries( + entry("auto-WILDCARD-MX-RR", false), + entry("auto-WILDCARD-A-RR", false), + entry("auto-WILDCARD-AAAA-RR", false), + entry("auto-WILDCARD-DKIM-RR", false), + entry("auto-WILDCARD-SPF-RR", false), + entry("user-RR", Array.of( + "www IN CNAME example.com. ; www.example.com is an alias for example.com", + "test1 IN 1h30m CNAME example.com.", + "test2 1h30m IN CNAME example.com.", + "ns IN A 192.0.2.2; IPv4 address for ns.example.com") + ) + )) + .build()), + """ + [ + { + "type": "DOMAIN_DNS_SETUP", + "identifier": "example.org", + "caption": "some fake Domain-DNS-Setup", + "alarmContact": null, + "config": { + "auto-WILDCARD-AAAA-RR": false, + "auto-WILDCARD-MX-RR": false, + "auto-WILDCARD-SPF-RR": false, + "auto-WILDCARD-DKIM-RR": false, + "auto-WILDCARD-A-RR": false, + "user-RR": [ + "www IN CNAME example.com. ; www.example.com is an alias for example.com", + "test1 IN 1h30m CNAME example.com.", + "test2 1h30m IN CNAME example.com.", + "ns IN A 192.0.2.2; IPv4 address for ns.example.com" + ] + } + } + ] """); final HsHostingAssetType assetType; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java index e8323839..bd571075 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java @@ -35,7 +35,9 @@ class HsHostingAssetPropsControllerAcceptanceTest { "MANAGED_WEBSPACE", "CLOUD_SERVER", "UNIX_USER", - "EMAIL_ALIAS" + "EMAIL_ALIAS", + "DOMAIN_SETUP", + "DOMAIN_DNS_SETUP" ] """)); // @formatter:on 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 cc8a029b..579257a0 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 @@ -27,6 +27,7 @@ import java.util.Map; import static java.util.Map.entry; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP; 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.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf; @@ -129,6 +130,9 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu .containsExactlyInAnyOrder(fromFormatted( initialGrantNames, + // global-admin + "{ grant perm:hs_hosting_asset#fir00:SELECT to role:global#global:ADMIN by system and assume }", // workaround + // owner "{ grant role:hs_hosting_asset#fir00:OWNER to role:hs_booking_item#fir01:ADMIN by system and assume }", "{ grant role:hs_hosting_asset#fir00:OWNER to role:hs_hosting_asset#vm1011:ADMIN by system and assume }", @@ -137,7 +141,6 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu // admin "{ grant role:hs_hosting_asset#fir00:ADMIN to role:hs_hosting_asset#fir00:OWNER by system and assume }", "{ grant role:hs_hosting_asset#fir00:ADMIN to role:hs_booking_item#fir01:AGENT by system and assume }", - "{ grant perm:hs_hosting_asset#fir00:INSERT>hs_hosting_asset to role:hs_hosting_asset#fir00:ADMIN by system and assume }", "{ grant perm:hs_hosting_asset#fir00:UPDATE to role:hs_hosting_asset#fir00:ADMIN by system and assume }", // agent @@ -148,17 +151,44 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu "{ grant role:hs_booking_item#fir01:TENANT to role:hs_hosting_asset#fir00:TENANT by system and assume }", "{ grant role:hs_hosting_asset#fir00:TENANT to role:hs_hosting_asset#fir00:AGENT by system and assume }", "{ grant role:hs_hosting_asset#vm1011:TENANT to role:hs_hosting_asset#fir00:TENANT by system and assume }", - "{ grant perm:hs_hosting_asset#fir00:SELECT to role:hs_hosting_asset#fir00:TENANT by system and assume }", + "{ grant perm:hs_hosting_asset#fir00:SELECT to role:hs_hosting_asset#fir00:TENANT by system and assume }", // workaround null)); } + @Test + public void anyUser_canCreateNewDomainSetupAsset() { + // given + context("superuser-alex@hostsharing.net"); + final var assetCount = assetRepo.count(); + + // when + context("person-SmithPeter@example.com"); + final var result = attempt(em, () -> { + final var newAsset = HsHostingAssetEntity.builder() + .caption("some new domain setup") + .type(DOMAIN_SETUP) + .identifier("example.org") + .build(); + return toCleanup(assetRepo.save(newAsset)); + }); + + // then + result.assertSuccessful(); + assertThat(result.returnedValue()).isNotNull().extracting(HsHostingAssetEntity::getUuid).isNotNull(); + assertThat(result.returnedValue().isLoaded()).isFalse(); + context("superuser-alex@hostsharing.net"); + assertThatAssetIsPersisted(result.returnedValue()); + assertThat(assetRepo.count()).isEqualTo(assetCount + 1); + } + private void assertThatAssetIsPersisted(final HsHostingAssetEntity saved) { + final var context = attempt(em, () -> { - context("superuser-alex@hostsharing.net"); final var found = assetRepo.findByUuid(saved.getUuid()); assertThat(found).isNotEmpty().map(HsHostingAssetEntity::toString).get().isEqualTo(saved.toString()); }); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java new file mode 100644 index 00000000..671b9452 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java @@ -0,0 +1,245 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity.HsHostingAssetEntityBuilder; +import net.hostsharing.hsadminng.mapper.Array; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_DNS_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsDomainDnsSetupHostingAssetValidator.RR_COMMENT; +import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsDomainDnsSetupHostingAssetValidator.RR_RECORD_DATA; +import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsDomainDnsSetupHostingAssetValidator.RR_RECORD_TYPE; +import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsDomainDnsSetupHostingAssetValidator.RR_REGEX_IN; +import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsDomainDnsSetupHostingAssetValidator.RR_REGEX_NAME; +import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsDomainDnsSetupHostingAssetValidator.RR_REGEX_TTL; +import static org.assertj.core.api.Assertions.assertThat; + +class HsDomainDnsSetupHostingAssetValidatorUnitTest { + + static final HsHostingAssetEntity validDomainSetupEntity = HsHostingAssetEntity.builder() + .type(DOMAIN_SETUP) + .identifier("example.org") + .build(); + + static HsHostingAssetEntityBuilder validEntityBuilder() { + return HsHostingAssetEntity.builder() + .type(DOMAIN_DNS_SETUP) + .parentAsset(validDomainSetupEntity) + .identifier("example.org") + .config(Map.ofEntries( + entry("user-RR", Array.of( + "@ 1814400 IN XXX example.org. root.example.org ( 1234 10800 900 604800 86400 )", + "www IN CNAME example.com. ; www.example.com is an alias for example.com", + "test1 IN 1h30m CNAME example.com.", + "test2 1h30m IN CNAME example.com.", + "ns IN A 192.0.2.2; IPv4 address for ns.example.com") + ) + )); + } + + @Test + void containsExpectedProperties() { + // when + final var validator = HsHostingAssetEntityValidatorRegistry.forType(DOMAIN_DNS_SETUP); + + // then + assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( + "{type=integer, propertyName=TTL, min=0, defaultValue=21600}", + "{type=boolean, propertyName=auto-SOA-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-NS-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-MX-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-A-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-AAAA-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-MAILSERVICES-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-AUTOCONFIG-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-AUTODISCOVER-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-DKIM-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-SPF-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-WILDCARD-MX-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-WILDCARD-A-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-WILDCARD-AAAA-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-WILDCARD-DKIM-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-WILDCARD-SPF-RR, defaultValue=true}", + "{type=string[], propertyName=user-RR, elementsOf={type=string, propertyName=user-RR, matchesRegEx=[([a-z0-9\\.-]+|@)\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*IN\\s+[A-Z]+\\s+[^;].*(;.*)*, ([a-z0-9\\.-]+|@)\\s+IN\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*[A-Z]+\\s+[^;].*(;.*)*], required=true}}" + ); + } + + @Test + void preprocessesTakesIdentifierFromParent() { + // given + final var givenEntity = validEntityBuilder().build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + validator.preprocessEntity(givenEntity); + + // then + assertThat(givenEntity.getIdentifier()).isEqualTo(givenEntity.getParentAsset().getIdentifier()); + } + + @Test + void rejectsInvalidIdentifier() { + // given + final var givenEntity = validEntityBuilder().identifier("wrong.org").build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + final var result = validator.validateEntity(givenEntity); + + // then + assertThat(result).containsExactly( + "'identifier' expected to match '^example.org$', but is 'wrong.org'" + ); + } + + @Test + void acceptsValidIdentifier() { + // given + final var givenEntity = validEntityBuilder().identifier(validDomainSetupEntity.getIdentifier()).build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + final var result = validator.validateEntity(givenEntity); + + // then + assertThat(result).isEmpty(); + } + + @Test + void validatesReferencedEntities() { + // given + final var mangedServerHostingAssetEntity = validEntityBuilder() + .parentAsset(HsHostingAssetEntity.builder().build()) + .assignedToAsset(HsHostingAssetEntity.builder().build()) + .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build()) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(mangedServerHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'DOMAIN_DNS_SETUP:example.org.bookingItem' must be null but is set to D-???????-?:null", + "'DOMAIN_DNS_SETUP:example.org.parentAsset' must be of type DOMAIN_SETUP but is of type null", + "'DOMAIN_DNS_SETUP:example.org.assignedToAsset' must be null but is set to D-???????-?:null"); + } + + @Test + void acceptsValidEntity() { + // given + final var givenEntity = validEntityBuilder().build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + final var errors = validator.validateEntity(givenEntity); + + // then + assertThat(errors).isEmpty(); + } + + @Test + void recectsInvalidProperties() { + // given + final var mangedServerHostingAssetEntity = validEntityBuilder() + .config(Map.ofEntries( + entry("TTL", "1d30m"), // currently only an integer for seconds is implemented here + entry("user-RR", Array.of( + "@ 1814400 IN 1814400 BAD1 TTL only allowed once", + "www BAD1 Record-Class missing / not enough columns")) + )) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(mangedServerHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'DOMAIN_DNS_SETUP:example.org.config.TTL' is expected to be of type class java.lang.Integer, but is of type 'String'", + "'DOMAIN_DNS_SETUP:example.org.config.user-RR' is expected to match any of [([a-z0-9\\.-]+|@)\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*IN\\s+[A-Z]+\\s+[^;].*(;.*)*, ([a-z0-9\\.-]+|@)\\s+IN\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*[A-Z]+\\s+[^;].*(;.*)*] but '@ 1814400 IN 1814400 BAD1 TTL only allowed once' does not match any", + "'DOMAIN_DNS_SETUP:example.org.config.user-RR' is expected to match any of [([a-z0-9\\.-]+|@)\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*IN\\s+[A-Z]+\\s+[^;].*(;.*)*, ([a-z0-9\\.-]+|@)\\s+IN\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*[A-Z]+\\s+[^;].*(;.*)*] but 'www BAD1 Record-Class missing / not enough columns' does not match any"); + } + + @Test + void validStringMatchesRegEx() { + assertThat("@ ").matches(RR_REGEX_NAME); + assertThat("ns ").matches(RR_REGEX_NAME); + assertThat("example.com. ").matches(RR_REGEX_NAME); + + assertThat("12400 ").matches(RR_REGEX_TTL); + assertThat("12400\t\t ").matches(RR_REGEX_TTL); + assertThat("12400 \t\t").matches(RR_REGEX_TTL); + assertThat("1h30m ").matches(RR_REGEX_TTL); + assertThat("30m ").matches(RR_REGEX_TTL); + + assertThat("IN ").matches(RR_REGEX_IN); + assertThat("IN\t\t ").matches(RR_REGEX_IN); + assertThat("IN \t\t").matches(RR_REGEX_IN); + + assertThat("CNAME ").matches(RR_RECORD_TYPE); + assertThat("CNAME\t\t ").matches(RR_RECORD_TYPE); + assertThat("CNAME \t\t").matches(RR_RECORD_TYPE); + + assertThat("example.com.").matches(RR_RECORD_DATA); + assertThat("123.123.123.123").matches(RR_RECORD_DATA); + assertThat("(some more complex argument in parenthesis)").matches(RR_RECORD_DATA); + assertThat("\"some more complex argument; including a semicolon\"").matches(RR_RECORD_DATA); + + assertThat("; whatever ; \" really anything").matches(RR_COMMENT); + } + + @Test + void generatesZonefile() { + // given + final var givenEntity = validEntityBuilder().build(); + final var validator = (HsDomainDnsSetupHostingAssetValidator) HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + final var zonefile = validator.toZonefileString(givenEntity); + + // then + assertThat(zonefile).isEqualTo(""" + $ORIGIN example.org. + $TTL 21600 + + ; these records are just placeholders to create a valid zonefile for the validation + @ 1814400 IN SOA example.org. root.example.org ( 1999010100 10800 900 604800 86400 ) + @ IN NS ns + + @ 1814400 IN XXX example.org. root.example.org ( 1234 10800 900 604800 86400 ) + www IN CNAME example.com. ; www.example.com is an alias for example.com + test1 IN 1h30m CNAME example.com. + test2 1h30m IN CNAME example.com. + ns IN A 192.0.2.2; IPv4 address for ns.example.com + """); + } + + @Test + void rejectsInvalidZonefile() { + // given + final var givenEntity = validEntityBuilder().config(Map.ofEntries( + entry("user-RR", Array.of( + "example.org. 1814400 IN SOA example.org. root.example.org (1234 10800 900 604800 86400)" + )) + )) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + final var errors = validator.validateContext(givenEntity); + + // then + assertThat(errors).containsExactlyInAnyOrder( + "dns_master_load: example.org: multiple RRs of singleton type", + "zone example.org/IN: loading from master file (null) failed: multiple RRs of singleton type", + "zone example.org/IN: not loaded due to errors." + ); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java new file mode 100644 index 00000000..b7d78567 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java @@ -0,0 +1,111 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity.HsHostingAssetEntityBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import java.util.Map; + +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP; +import static org.assertj.core.api.Assertions.assertThat; + +class HsDomainSetupHostingAssetValidatorUnitTest { + + static HsHostingAssetEntityBuilder validEntityBuilder() { + return HsHostingAssetEntity.builder() + .type(DOMAIN_SETUP) + .identifier("example.org"); + } + + enum InvalidDomainNameIdentifier { + EMPTY(""), + TOO_LONG("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456890123456789.de"), + DASH_AT_BEGINNING("-example.com"), + DOT_AT_BEGINNING(".example.com"), + DOT_AT_END("example.com."); + + final String domainName; + + InvalidDomainNameIdentifier(final String domainName) { + this.domainName = domainName; + } + } + + @ParameterizedTest + @EnumSource(InvalidDomainNameIdentifier.class) + void rejectsInvalidIdentifier(final InvalidDomainNameIdentifier testCase) { + // given + final var givenEntity = validEntityBuilder().identifier(testCase.domainName).build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + final var result = validator.validateEntity(givenEntity); + + // then + assertThat(result).containsExactly( + "'identifier' expected to match '^((?!-)[A-Za-z0-9-]{1,63}(?&2"); + + // when + final var returnCode = process.execute(); + + // then + assertThat(returnCode).isEqualTo(0); + assertThat(process.getStdOut()).isEqualTo("Hello, World!\n"); + assertThat(process.getStdErr()).isEqualTo("Error!\n"); + } + + @Test + @EnabledOnOs(LINUX) + void shouldReturnErrorCode() throws IOException, InterruptedException { + // given + final var process = new SystemProcess("false"); + + // when + final int returnCode = process.execute(); + + // then + assertThat(returnCode).isEqualTo(1); + } + + @Test + @EnabledOnOs(LINUX) + void shouldExecuteAndFeedInput() throws IOException, InterruptedException { + // given + final var process = new SystemProcess("tr", "[:lower:]", "[:upper:]"); + + // when + final int returnCode = process.execute("Hallo"); + + // then + assertThat(returnCode).isEqualTo(0); + assertThat(process.getStdOut()).isEqualTo("HALLO\n"); + } + + @Test + void shouldThrowExceptionIfProgramNotFound() { + // given + final var process = new SystemProcess("non-existing program"); + + // when + final var exception = catchThrowable(process::execute); + + // then + assertThat(exception).isInstanceOf(IOException.class) + .hasMessage("Cannot run program \"non-existing program\": error=2, No such file or directory"); + } + + @Test + void shouldBeAbleToRunMultipleTimes() throws IOException, InterruptedException { + // given + final var process = new SystemProcess("true"); + + // when + process.execute(); + final int returnCode = process.execute(); + + // then + assertThat(returnCode).isEqualTo(0); + } +} From afb6771ed777eb233b68b3041ee3ce4575cb9ec6 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 9 Jul 2024 14:32:14 +0200 Subject: [PATCH 16/18] HostingAsset-Hierarchie spec in enum HsHostingAssetType and generates PlantUML (#72) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/72 Reviewed-by: Timotheus Pokorra --- doc/hs-hosting-asset-type-structure.md | 199 ++++++++++ .../hs/booking/item/HsBookingItemType.java | 37 +- .../hsadminng/hs/booking/item/Node.java | 9 + ...HsManagedWebspaceBookingItemValidator.java | 4 +- .../hosting/asset/HsHostingAssetEntity.java | 4 +- .../hs/hosting/asset/HsHostingAssetType.java | 353 +++++++++++++++++- .../HsCloudServerHostingAssetValidator.java | 7 +- ...HsDomainDnsSetupHostingAssetValidator.java | 20 +- .../HsDomainSetupHostingAssetValidator.java | 37 +- .../HsEMailAliasHostingAssetValidator.java | 4 +- .../HsHostingAssetEntityValidator.java | 183 ++++----- .../HsManagedServerHostingAssetValidator.java | 6 +- ...sManagedWebspaceHostingAssetValidator.java | 9 +- .../HsUnixUserHostingAssetValidator.java | 5 +- .../hs/validation/HsEntityValidator.java | 3 +- .../db/changelog/0-basis/010-context.sql | 2 +- .../7013-hs-hosting-asset-rbac.md | 7 +- .../7013-hs-hosting-asset-rbac.sql | 8 +- .../7018-hs-hosting-asset-test-data.sql | 18 +- .../hsadminng/arch/ArchitectureTest.java | 16 + ...gedServerBookingItemValidatorUnitTest.java | 2 +- ...sHostingAssetControllerAcceptanceTest.java | 41 ++ ...HostingAssetRepositoryIntegrationTest.java | 28 +- .../asset/HsHostingAssetTypeUnitTest.java | 219 +++++++++++ ...udServerHostingAssetValidatorUnitTest.java | 14 +- ...DnsSetupHostingAssetValidatorUnitTest.java | 29 +- ...ainSetupHostingAssetValidatorUnitTest.java | 11 +- ...ailAliasHostingAssetValidatorUnitTest.java | 4 +- ...edServerHostingAssetValidatorUnitTest.java | 19 +- ...WebspaceHostingAssetValidatorUnitTest.java | 9 +- ...ssTest.java => SystemProcessUnitTest.java} | 2 +- 31 files changed, 1076 insertions(+), 233 deletions(-) create mode 100644 doc/hs-hosting-asset-type-structure.md create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/booking/item/Node.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetTypeUnitTest.java rename src/test/java/net/hostsharing/hsadminng/system/{SystemProcessTest.java => SystemProcessUnitTest.java} (98%) diff --git a/doc/hs-hosting-asset-type-structure.md b/doc/hs-hosting-asset-type-structure.md new file mode 100644 index 00000000..b03b7ced --- /dev/null +++ b/doc/hs-hosting-asset-type-structure.md @@ -0,0 +1,199 @@ +## HostingAsset Type Structure + +### Domain + +```plantuml +@startuml +left to right direction + +package Booking #feb28c { + entity BI_PRIVATE_CLOUD + entity BI_CLOUD_SERVER + entity BI_MANAGED_SERVER + entity BI_MANAGED_WEBSPACE + entity BI_DOMAIN_DNS_SETUP + entity BI_DOMAIN_EMAIL_SUBMISSION_SETUP +} + +package Hosting #feb28c{ + package Domain #99bcdb { + entity HA_DOMAIN_SETUP + entity HA_DOMAIN_DNS_SETUP + entity HA_DOMAIN_HTTP_SETUP + entity HA_DOMAIN_EMAIL_SUBMISSION_SETUP + entity HA_DOMAIN_EMAIL_MAILBOX_SETUP + entity HA_EMAIL_ADDRESS + } + + package Server #99bcdb { + entity HA_CLOUD_SERVER + entity HA_MANAGED_SERVER + entity HA_IP_NUMBER + } + + package Webspace #99bcdb { + entity HA_MANAGED_WEBSPACE + entity HA_UNIX_USER + entity HA_EMAIL_ALIAS + } + +} + +BI_CLOUD_SERVER *--> BI_PRIVATE_CLOUD +BI_MANAGED_SERVER *--> BI_PRIVATE_CLOUD +BI_MANAGED_WEBSPACE *--> BI_MANAGED_SERVER + +HA_CLOUD_SERVER ==* BI_CLOUD_SERVER +HA_MANAGED_SERVER ==* BI_MANAGED_SERVER +HA_MANAGED_WEBSPACE ==* BI_MANAGED_WEBSPACE +HA_MANAGED_WEBSPACE o..> HA_MANAGED_SERVER +HA_UNIX_USER *==> HA_MANAGED_WEBSPACE +HA_DOMAIN_SETUP o..> HA_DOMAIN_SETUP +HA_DOMAIN_DNS_SETUP *==> HA_DOMAIN_SETUP +HA_DOMAIN_HTTP_SETUP *==> HA_DOMAIN_SETUP +HA_DOMAIN_HTTP_SETUP o..> HA_UNIX_USER +HA_DOMAIN_EMAIL_SUBMISSION_SETUP *==> HA_DOMAIN_SETUP +HA_DOMAIN_EMAIL_SUBMISSION_SETUP o..> HA_MANAGED_WEBSPACE +HA_DOMAIN_EMAIL_MAILBOX_SETUP *==> HA_DOMAIN_SETUP +HA_DOMAIN_EMAIL_MAILBOX_SETUP o..> HA_MANAGED_WEBSPACE +HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE +HA_EMAIL_ADDRESS *==> HA_DOMAIN_EMAIL_MAILBOX_SETUP +HA_IP_NUMBER o..> HA_CLOUD_SERVER +HA_IP_NUMBER o..> HA_MANAGED_SERVER +HA_IP_NUMBER o..> HA_MANAGED_WEBSPACE + +package Legend #white { + SUB_ENTITY1 *--> REQUIRED_PARENT_ENTITY + SUB_ENTITY2 *..> OPTIONAL_PARENT_ENTITY + ASSIGNED_ENTITY1 o--> REQUIRED_ASSIGNED_TO_ENTITY1 + ASSIGNED_ENTITY2 o..> OPTIONAL_ASSIGNED_TO_ENTITY2 +} +Booking -down[hidden]->Legend +``` +### MariaDB + +```plantuml +@startuml +left to right direction + +package Booking #feb28c { + entity BI_PRIVATE_CLOUD + entity BI_CLOUD_SERVER + entity BI_MANAGED_SERVER + entity BI_MANAGED_WEBSPACE + entity BI_DOMAIN_DNS_SETUP + entity BI_DOMAIN_EMAIL_SUBMISSION_SETUP +} + +package Hosting #feb28c{ + package MariaDB #99bcdb { + entity HA_MARIADB_INSTANCE + entity HA_MARIADB_USER + entity HA_MARIADB_DATABASE + } + + package Server #99bcdb { + entity HA_CLOUD_SERVER + entity HA_MANAGED_SERVER + entity HA_IP_NUMBER + } + + package Webspace #99bcdb { + entity HA_MANAGED_WEBSPACE + entity HA_UNIX_USER + entity HA_EMAIL_ALIAS + } + +} + +BI_CLOUD_SERVER *--> BI_PRIVATE_CLOUD +BI_MANAGED_SERVER *--> BI_PRIVATE_CLOUD +BI_MANAGED_WEBSPACE *--> BI_MANAGED_SERVER + +HA_CLOUD_SERVER ==* BI_CLOUD_SERVER +HA_MANAGED_SERVER ==* BI_MANAGED_SERVER +HA_MANAGED_WEBSPACE ==* BI_MANAGED_WEBSPACE +HA_MANAGED_WEBSPACE o..> HA_MANAGED_SERVER +HA_UNIX_USER *==> HA_MANAGED_WEBSPACE +HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE +HA_MARIADB_INSTANCE *==> HA_MANAGED_SERVER +HA_MARIADB_USER *==> HA_MARIADB_INSTANCE +HA_MARIADB_USER o..> HA_MANAGED_WEBSPACE +HA_MARIADB_DATABASE *==> HA_MANAGED_WEBSPACE +HA_MARIADB_DATABASE o..> HA_MARIADB_INSTANCE +HA_IP_NUMBER o..> HA_CLOUD_SERVER +HA_IP_NUMBER o..> HA_MANAGED_SERVER +HA_IP_NUMBER o..> HA_MANAGED_WEBSPACE + +package Legend #white { + SUB_ENTITY1 *--> REQUIRED_PARENT_ENTITY + SUB_ENTITY2 *..> OPTIONAL_PARENT_ENTITY + ASSIGNED_ENTITY1 o--> REQUIRED_ASSIGNED_TO_ENTITY1 + ASSIGNED_ENTITY2 o..> OPTIONAL_ASSIGNED_TO_ENTITY2 +} +Booking -down[hidden]->Legend +``` +### PostgreSQL + +```plantuml +@startuml +left to right direction + +package Booking #feb28c { + entity BI_PRIVATE_CLOUD + entity BI_CLOUD_SERVER + entity BI_MANAGED_SERVER + entity BI_MANAGED_WEBSPACE + entity BI_DOMAIN_DNS_SETUP + entity BI_DOMAIN_EMAIL_SUBMISSION_SETUP +} + +package Hosting #feb28c{ + package PostgreSQL #99bcdb { + entity HA_PGSQL_INSTANCE + entity HA_PGSQL_USER + entity HA_PGSQL_DATABASE + } + + package Server #99bcdb { + entity HA_CLOUD_SERVER + entity HA_MANAGED_SERVER + entity HA_IP_NUMBER + } + + package Webspace #99bcdb { + entity HA_MANAGED_WEBSPACE + entity HA_UNIX_USER + entity HA_EMAIL_ALIAS + } + +} + +BI_CLOUD_SERVER *--> BI_PRIVATE_CLOUD +BI_MANAGED_SERVER *--> BI_PRIVATE_CLOUD +BI_MANAGED_WEBSPACE *--> BI_MANAGED_SERVER + +HA_CLOUD_SERVER ==* BI_CLOUD_SERVER +HA_MANAGED_SERVER ==* BI_MANAGED_SERVER +HA_MANAGED_WEBSPACE ==* BI_MANAGED_WEBSPACE +HA_MANAGED_WEBSPACE o..> HA_MANAGED_SERVER +HA_UNIX_USER *==> HA_MANAGED_WEBSPACE +HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE +HA_PGSQL_INSTANCE *==> HA_MANAGED_SERVER +HA_PGSQL_USER *==> HA_PGSQL_INSTANCE +HA_PGSQL_USER o..> HA_MANAGED_WEBSPACE +HA_PGSQL_DATABASE *==> HA_MANAGED_WEBSPACE +HA_PGSQL_DATABASE o..> HA_PGSQL_INSTANCE +HA_IP_NUMBER o..> HA_CLOUD_SERVER +HA_IP_NUMBER o..> HA_MANAGED_SERVER +HA_IP_NUMBER o..> HA_MANAGED_WEBSPACE + +package Legend #white { + SUB_ENTITY1 *--> REQUIRED_PARENT_ENTITY + SUB_ENTITY2 *..> OPTIONAL_PARENT_ENTITY + ASSIGNED_ENTITY1 o--> REQUIRED_ASSIGNED_TO_ENTITY1 + ASSIGNED_ENTITY2 o..> OPTIONAL_ASSIGNED_TO_ENTITY2 +} +Booking -down[hidden]->Legend +``` + This code generated was by HsHostingAssetType.main, do not amend manually. diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemType.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemType.java index 719ce75b..720b3ecc 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemType.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemType.java @@ -1,8 +1,37 @@ package net.hostsharing.hsadminng.hs.booking.item; -public enum HsBookingItemType { +import java.util.List; + +import static java.util.Optional.ofNullable; + +public enum HsBookingItemType implements Node { PRIVATE_CLOUD, - CLOUD_SERVER, - MANAGED_SERVER, - MANAGED_WEBSPACE + CLOUD_SERVER(PRIVATE_CLOUD), + MANAGED_SERVER(PRIVATE_CLOUD), + MANAGED_WEBSPACE(MANAGED_SERVER), + DOMAIN_DNS_SETUP, // TODO.spec: experimental + DOMAIN_EMAIL_SUBMISSION_SETUP; // TODO.spec: experimental + + private final HsBookingItemType parentItemType; + + HsBookingItemType() { + this.parentItemType = null; + } + + HsBookingItemType(final HsBookingItemType parentItemType) { + this.parentItemType = parentItemType; + } + + @Override + public List edges() { + return ofNullable(parentItemType) + .map(p -> (nodeName() + " *--> " + p.nodeName())) + .stream().toList(); + } + + @Override + public String nodeName() { + return "BI_" + name(); + } + } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/Node.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/Node.java new file mode 100644 index 00000000..cca14f5a --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/Node.java @@ -0,0 +1,9 @@ +package net.hostsharing.hsadminng.hs.booking.item; + +import java.util.List; + +public interface Node { + + String nodeName(); + List edges(); +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java index 81c74b9f..2bca0042 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java @@ -8,7 +8,7 @@ import java.util.List; import static java.util.Collections.emptyList; import static java.util.Optional.ofNullable; -import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_EMAIL_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_EMAIL_MAILBOX_SETUP; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.EMAIL_ADDRESS; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_DATABASE; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_USER; @@ -88,7 +88,7 @@ class HsManagedWebspaceBookingItemValidator extends HsBookingItemEntityValidator return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> { final var unixUserCount = ofNullable(entity.getRelatedHostingAsset()) .map(ha -> ha.getSubHostingAssets().stream() - .filter(bi -> bi.getType() == DOMAIN_EMAIL_SETUP) + .filter(bi -> bi.getType() == DOMAIN_EMAIL_MAILBOX_SETUP) .flatMap(domainEMailSetup -> domainEMailSetup.getSubHostingAssets().stream() .filter(subAsset -> subAsset.getType()==EMAIL_ADDRESS)) .count()) 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 80f9294c..55b8d00e 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 @@ -50,6 +50,7 @@ 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; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.UPDATE; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.GUEST; @@ -204,11 +205,12 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Properti .switchOnColumn("type", inCaseOf("DOMAIN_SETUP", then -> { then.toRole(GLOBAL, GUEST).grantPermission(INSERT); - then.toRole(GLOBAL, ADMIN).grantPermission(SELECT); // TODO.spec: replace by a proper solution }) ) .createRole(OWNER, (with) -> { + with.owningUser(CREATOR); + with.incomingSuperRole(GLOBAL, ADMIN).unassumed(); // TODO.spec: replace by a better solution with.incomingSuperRole("bookingItem", ADMIN); with.incomingSuperRole("parentAsset", ADMIN); with.permission(DELETE); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java index 88ccca45..6a0846a9 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java @@ -1,33 +1,204 @@ package net.hostsharing.hsadminng.hs.hosting.asset; +import lombok.AllArgsConstructor; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; +import net.hostsharing.hsadminng.hs.booking.item.Node; -public enum HsHostingAssetType { - CLOUD_SERVER, // named e.g. vm1234 - MANAGED_SERVER, // named e.g. vm1234 - MANAGED_WEBSPACE(MANAGED_SERVER), // named eg. xyz00 - UNIX_USER(MANAGED_WEBSPACE), // named e.g. xyz00-abc - DOMAIN_SETUP, // named e.g. example.org - DOMAIN_DNS_SETUP(DOMAIN_SETUP), // named e.g. example.org - DOMAIN_HTTP_SETUP(DOMAIN_SETUP), // named e.g. example.org - DOMAIN_EMAIL_SETUP(DOMAIN_SETUP), // named e.g. example.org +import javax.naming.NamingException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; +import static net.hostsharing.hsadminng.hs.hosting.asset.EntityTypeRelation.*; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.RelationPolicy.OPTIONAL; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.RelationPolicy.REQUIRED; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.RelationType.ASSIGNED_TO_ASSET; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.RelationType.BOOKING_ITEM; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.RelationType.PARENT_ASSET; + +public enum HsHostingAssetType implements Node { + SAME_TYPE, // pseudo-type for recursive references + + CLOUD_SERVER( // named e.g. vm1234 + inGroup("Server"), + requires(HsBookingItemType.CLOUD_SERVER)), + + MANAGED_SERVER( // named e.g. vm1234 + inGroup("Server"), + requires(HsBookingItemType.MANAGED_SERVER)), + + MANAGED_WEBSPACE( // named eg. xyz00 + inGroup("Webspace"), + requires(HsBookingItemType.MANAGED_WEBSPACE), + optionalParent(MANAGED_SERVER)), + + UNIX_USER( // named e.g. xyz00-abc + inGroup("Webspace"), + requiredParent(MANAGED_WEBSPACE)), + + DOMAIN_SETUP( // named e.g. example.org + inGroup("Domain"), + optionalParent(SAME_TYPE) + ), + + DOMAIN_DNS_SETUP( // named e.g. example.org + inGroup("Domain"), + requiredParent(DOMAIN_SETUP)), + + DOMAIN_HTTP_SETUP( // named e.g. example.org + inGroup("Domain"), + requiredParent(DOMAIN_SETUP), + assignedTo(UNIX_USER)), + + DOMAIN_EMAIL_SUBMISSION_SETUP( // named e.g. example.org + inGroup("Domain"), + requiredParent(DOMAIN_SETUP), + assignedTo(MANAGED_WEBSPACE)), + + DOMAIN_EMAIL_MAILBOX_SETUP( // named e.g. example.org + inGroup("Domain"), + requiredParent(DOMAIN_SETUP), + assignedTo(MANAGED_WEBSPACE)), // TODO.spec: SECURE_MX - EMAIL_ALIAS(MANAGED_WEBSPACE), // named e.g. xyz00-abc - EMAIL_ADDRESS(DOMAIN_EMAIL_SETUP), // named e.g. sample@example.org - PGSQL_USER(MANAGED_WEBSPACE), // named e.g. xyz00_abc - PGSQL_DATABASE(MANAGED_WEBSPACE), // named e.g. xyz00_abc, TODO.spec: or PGSQL_USER? - MARIADB_USER(MANAGED_WEBSPACE), // named e.g. xyz00_abc - MARIADB_DATABASE(MANAGED_WEBSPACE); // named e.g. xyz00_abc, TODO.spec: or MARIADB_USER? + EMAIL_ALIAS( // named e.g. xyz00-abc + inGroup("Webspace"), + requiredParent(MANAGED_WEBSPACE)), - public final HsHostingAssetType parentAssetType; + EMAIL_ADDRESS( // named e.g. sample@example.org + inGroup("Domain"), + requiredParent(DOMAIN_EMAIL_MAILBOX_SETUP)), - HsHostingAssetType(final HsHostingAssetType parentAssetType) { - this.parentAssetType = parentAssetType; + PGSQL_INSTANCE( // TODO.spec: identifier to be specified + inGroup("PostgreSQL"), + requiredParent(MANAGED_SERVER)), + + PGSQL_USER( // named e.g. xyz00_abc + inGroup("PostgreSQL"), + requiredParent(PGSQL_INSTANCE), + assignedTo(MANAGED_WEBSPACE)), + + PGSQL_DATABASE( // named e.g. xyz00_abc + inGroup("PostgreSQL"), + requiredParent(MANAGED_WEBSPACE), // TODO.spec: or PGSQL_USER? + assignedTo(PGSQL_INSTANCE)), // TODO.spec: or swapping parent+assignedTo? + + MARIADB_INSTANCE( // TODO.spec: identifier to be specified + inGroup("MariaDB"), + requiredParent(MANAGED_SERVER)), // TODO.spec: or MANAGED_WEBSPACE? + + MARIADB_USER( // named e.g. xyz00_abc + inGroup("MariaDB"), + requiredParent(MARIADB_INSTANCE), + assignedTo(MANAGED_WEBSPACE)), + + MARIADB_DATABASE( // named e.g. xyz00_abc + inGroup("MariaDB"), + requiredParent(MANAGED_WEBSPACE), // TODO.spec: or MARIADB_USER? + assignedTo(MARIADB_INSTANCE)), // TODO.spec: or swapping parent+assignedTo? + + IP_NUMBER( + inGroup("Server"), + assignedTo(CLOUD_SERVER), + assignedTo(MANAGED_SERVER), + assignedTo(MANAGED_WEBSPACE) + ); + + private final String groupName; + private final EntityTypeRelation[] relations; + + HsHostingAssetType( + final String groupName, + final EntityTypeRelation... relations + ) { + this.groupName = groupName; + this.relations = relations; } HsHostingAssetType() { - this(null); + this.groupName = null; + this.relations = null; + } + + /// just syntactic sugar + private static String inGroup(final String groupName) { + return groupName; + } + + // TODO.refa: try to get rid of the following similar methods: + + public RelationPolicy bookingItemPolicy() { + return stream(relations) + .filter(r -> r.relationType == BOOKING_ITEM) + .map(r -> r.relationPolicy) + .reduce(HsHostingAssetType::onlyASingleElementExpectedException) + .orElse(RelationPolicy.FORBIDDEN); + } + + public HsBookingItemType bookingItemType() { + return stream(relations) + .filter(r -> r.relationType == BOOKING_ITEM) + .map(r -> HsBookingItemType.valueOf(r.relatedType(this).toString())) + .reduce(HsHostingAssetType::onlyASingleElementExpectedException) + .orElse(null); + } + + public RelationPolicy parentAssetPolicy() { + return stream(relations) + .filter(r -> r.relationType == PARENT_ASSET) + .map(r -> r.relationPolicy) + .reduce(HsHostingAssetType::onlyASingleElementExpectedException) + .orElse(RelationPolicy.FORBIDDEN); + } + + public HsHostingAssetType parentAssetType() { + return stream(relations) + .filter(r -> r.relationType == PARENT_ASSET) + .map(r -> HsHostingAssetType.valueOf(r.relatedType(this).toString())) + .reduce(HsHostingAssetType::onlyASingleElementExpectedException) + .orElse(null); + } + + public RelationPolicy assignedToAssetPolicy() { + return stream(relations) + .filter(r -> r.relationType == ASSIGNED_TO_ASSET) + .map(r -> r.relationPolicy) + .reduce(HsHostingAssetType::onlyASingleElementExpectedException) + .orElse(RelationPolicy.FORBIDDEN); + } + + public HsHostingAssetType assignedToAssetType() { + return stream(relations) + .filter(r -> r.relationType == ASSIGNED_TO_ASSET) + .map(r -> HsHostingAssetType.valueOf(r.relatedType(this).toString())) + .reduce(HsHostingAssetType::onlyASingleElementExpectedException) + .orElse(null); + } + + private static X onlyASingleElementExpectedException(Object a, Object b) { + throw new IllegalStateException("Only a single element expected to match criteria."); + } + + @Override + public List edges() { + return stream(relations) + .map(r -> nodeName() + r.edge + r.relatedType(this).nodeName()) + .toList(); + } + + @Override + public String nodeName() { + return "HA_" + name(); } public static > HsHostingAssetType of(final T value) { @@ -37,4 +208,148 @@ public enum HsHostingAssetType { static String asString(final HsHostingAssetType type) { return type == null ? null : type.name(); } + + private static String renderAsPlantUML(final String caption, final Set includedHostingGroups) { + final String bookingNodes = stream(HsBookingItemType.values()) + .map(t -> " entity " + t.nodeName()) + .collect(joining("\n")); + final String hostingGroups = includedHostingGroups.stream().sorted() + .map(HsHostingAssetType::generateGroup) + .collect(joining("\n")); + final String hostingAssetNodes = stream(HsHostingAssetType.values()) + .filter(t -> t.isInGroups(includedHostingGroups)) + .map(t -> "entity " + t.nodeName()) + .collect(joining("\n")); + final String bookingItemEdges = stream(HsBookingItemType.values()) + .map(HsBookingItemType::edges) + .flatMap(Collection::stream) + .collect(joining("\n")); + final String hostingAssetEdges = stream(HsHostingAssetType.values()) + .filter(t -> t.isInGroups(includedHostingGroups)) + .map(HsHostingAssetType::edges) + .flatMap(Collection::stream) + .collect(joining("\n")); + return """ + + ### %{caption} + + ```plantuml + @startuml + left to right direction + + package Booking #feb28c { + %{bookingNodes} + } + + package Hosting #feb28c{ + %{hostingGroups} + } + + %{bookingItemEdges} + + %{hostingAssetEdges} + + package Legend #white { + SUB_ENTITY1 *--> REQUIRED_PARENT_ENTITY + SUB_ENTITY2 *..> OPTIONAL_PARENT_ENTITY + ASSIGNED_ENTITY1 o--> REQUIRED_ASSIGNED_TO_ENTITY1 + ASSIGNED_ENTITY2 o..> OPTIONAL_ASSIGNED_TO_ENTITY2 + } + Booking -down[hidden]->Legend + ``` + """ + .replace("%{caption}", caption) + .replace("%{bookingNodes}", bookingNodes) + .replace("%{hostingGroups}", hostingGroups) + .replace("%{hostingAssetNodeStyles}", hostingAssetNodes) + .replace("%{bookingItemEdges}", bookingItemEdges) + .replace("%{hostingAssetEdges}", hostingAssetEdges); + } + + private boolean isInGroups(final Set assetGroups) { + return groupName != null && assetGroups.contains(groupName); + } + + private static String generateGroup(final String group) { + return " package " + group + " #99bcdb {\n" + + stream(HsHostingAssetType.values()) + .filter(t -> group.equals(t.groupName)) + .map(t -> " entity " + t.nodeName()) + .collect(joining("\n")) + + "\n }\n"; + } + + static String renderAsEmbeddedPlantUml() { + + final var markdown = new StringBuilder(""" + ## HostingAsset Type Structure + + """); + + // rendering all types in a single diagram is currently ignored + renderAsPlantUML("Domain", stream(HsHostingAssetType.values()) + .filter(t -> t.groupName != null) + .map(t -> t.groupName) + .collect(toSet())); + + markdown.append(renderAsPlantUML("Domain", Set.of("Domain", "Webspace", "Server"))) + .append(renderAsPlantUML("MariaDB", Set.of("MariaDB", "Webspace", "Server"))) + .append(renderAsPlantUML("PostgreSQL", Set.of("PostgreSQL", "Webspace", "Server"))); + + markdown.append(""" + + This code generated was by %{this}.main, do not amend manually. + """ + .replace("%{this}", HsHostingAssetType.class.getSimpleName())); + + return markdown.toString(); + } + + public static void main(final String[] args) throws IOException, NamingException { + Files.writeString( + Path.of("doc/hs-hosting-asset-type-structure.md"), + renderAsEmbeddedPlantUml(), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } + + public enum RelationPolicy { + FORBIDDEN, OPTIONAL, REQUIRED + } + + public enum RelationType { + BOOKING_ITEM, + PARENT_ASSET, + ASSIGNED_TO_ASSET + } +} + +@AllArgsConstructor +class EntityTypeRelation { + + final HsHostingAssetType.RelationPolicy relationPolicy; + final HsHostingAssetType.RelationType relationType; + final Function getter; + private final T relatedType; + final String edge; + + public T relatedType(final HsHostingAssetType referringType) { + //noinspection unchecked + return relatedType == HsHostingAssetType.SAME_TYPE ? (T) referringType : relatedType; + } + + static EntityTypeRelation requires(final HsBookingItemType bookingItemType) { + return new EntityTypeRelation<>(REQUIRED, BOOKING_ITEM, HsHostingAssetEntity::getBookingItem, bookingItemType, " *==> "); + } + + static EntityTypeRelation optionalParent(final HsHostingAssetType hostingAssetType) { + return new EntityTypeRelation<>(OPTIONAL, PARENT_ASSET, HsHostingAssetEntity::getParentAsset, hostingAssetType, " o..> "); + } + + static EntityTypeRelation requiredParent(final HsHostingAssetType hostingAssetType) { + return new EntityTypeRelation<>(REQUIRED, PARENT_ASSET, HsHostingAssetEntity::getParentAsset, hostingAssetType, " *==> "); + } + + static EntityTypeRelation assignedTo(final HsHostingAssetType hostingAssetType) { + return new EntityTypeRelation<>(REQUIRED, ASSIGNED_TO_ASSET, HsHostingAssetEntity::getAssignedToAsset, hostingAssetType, " o..> "); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidator.java index 9144189b..9413dcf2 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidator.java @@ -1,17 +1,16 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import java.util.regex.Pattern; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER; + class HsCloudServerHostingAssetValidator extends HsHostingAssetEntityValidator { HsCloudServerHostingAssetValidator() { super( - BookingItem.mustBeOfType(HsBookingItemType.CLOUD_SERVER), - ParentAsset.mustBeNull(), - AssignedToAsset.mustBeNull(), + CLOUD_SERVER, AlarmContact.isOptional(), NO_EXTRA_PROPERTIES); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java index e09f77ef..c263be60 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java @@ -2,7 +2,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; import lombok.SneakyThrows; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; -import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import net.hostsharing.hsadminng.system.SystemProcess; import java.util.List; @@ -10,6 +9,7 @@ import java.util.regex.Pattern; import static java.util.Arrays.stream; import static java.util.Optional.ofNullable; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_DNS_SETUP; import static net.hostsharing.hsadminng.hs.validation.ArrayProperty.arrayOf; import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty; import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; @@ -30,11 +30,11 @@ class HsDomainDnsSetupHostingAssetValidator extends HsHostingAssetEntityValidato static final String RR_REGEX_IN_TTL = RR_REGEX_NAME + RR_REGEX_IN + RR_REGEX_TTL + RR_RECORD_TYPE + RR_RECORD_DATA + RR_COMMENT; + public static final String IDENTIFIER_SUFFIX = "|DNS"; HsDomainDnsSetupHostingAssetValidator() { - super( BookingItem.mustBeNull(), - ParentAsset.mustBeOfType(HsHostingAssetType.DOMAIN_SETUP), - AssignedToAsset.mustBeNull(), + super( + DOMAIN_DNS_SETUP, AlarmContact.isOptional(), integerProperty("TTL").min(0).withDefault(21600), @@ -60,14 +60,14 @@ class HsDomainDnsSetupHostingAssetValidator extends HsHostingAssetEntityValidato @Override protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { - return Pattern.compile("^" + assetEntity.getParentAsset().getIdentifier() + "$"); + return Pattern.compile("^" + assetEntity.getParentAsset().getIdentifier() + Pattern.quote(IDENTIFIER_SUFFIX) + "$"); } @Override public void preprocessEntity(final HsHostingAssetEntity entity) { super.preprocessEntity(entity); if (entity.getIdentifier() == null) { - ofNullable(entity.getParentAsset()).ifPresent(pa -> entity.setIdentifier(pa.getIdentifier())); + ofNullable(entity.getParentAsset()).ifPresent(pa -> entity.setIdentifier(pa.getIdentifier() + IDENTIFIER_SUFFIX)); } } @@ -77,7 +77,7 @@ class HsDomainDnsSetupHostingAssetValidator extends HsHostingAssetEntityValidato final var result = super.validateContext(assetEntity); // TODO.spec: define which checks should get raised to error level - final var namedCheckZone = new SystemProcess("named-checkzone", assetEntity.getIdentifier()); + final var namedCheckZone = new SystemProcess("named-checkzone", fqdn(assetEntity)); if (namedCheckZone.execute(toZonefileString(assetEntity)) != 0) { // yes, named-checkzone writes error messages to stdout stream(namedCheckZone.getStdOut().split("\n")) @@ -99,8 +99,12 @@ class HsDomainDnsSetupHostingAssetValidator extends HsHostingAssetEntityValidato {userRRs} """ - .replace("{domain}", assetEntity.getIdentifier()) + .replace("{domain}", fqdn(assetEntity)) .replace("{ttl}", getPropertyValue(assetEntity, "TTL")) .replace("{userRRs}", getPropertyValues(assetEntity, "user-RR") ); } + + private String fqdn(final HsHostingAssetEntity assetEntity) { + return assetEntity.getIdentifier().substring(0, assetEntity.getIdentifier().length()-IDENTIFIER_SUFFIX.length()); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java index d2693f7e..e16b1356 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java @@ -2,8 +2,11 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import java.util.List; import java.util.regex.Pattern; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP; + class HsDomainSetupHostingAssetValidator extends HsHostingAssetEntityValidator { public static final String DOMAIN_NAME_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(? validateEntity(final HsHostingAssetEntity assetEntity) { + // TODO.impl: for newly created entities, check the permission of setting up a domain + // + // reject, if the domain is any of these: + // hostsharing.com|net|org|coop, // just to be on the safe side + // [^.}+, // top-level-domain + // co.uk, org.uk, gov.uk, ac.uk, sch.uk, + // com.au, net.au, org.au, edu.au, gov.au, asn.au, id.au, + // co.jp, ne.jp, or.jp, ac.jp, go.jp, + // com.cn, net.cn, org.cn, gov.cn, edu.cn, ac.cn, + // com.br, net.br, org.br, gov.br, edu.br, mil.br, art.br, + // co.in, net.in, org.in, gen.in, firm.in, ind.in, + // com.mx, net.mx, org.mx, gob.mx, edu.mx, + // gov.it, edu.it, + // co.nz, net.nz, org.nz, govt.nz, ac.nz, school.nz, geek.nz, kiwi.nz, + // co.kr, ne.kr, or.kr, go.kr, re.kr, pe.kr + // + // allow if + // - user has Admin/Agent-role for all its sub-domains and the direct parent-Domain which are set up at at Hostsharing + // - domain has DNS zone with TXT record approval + // - parent-domain has DNS zone with TXT record approval + // - dom + // + // TXT-Record check: + // new InitialDirContext().getAttributes("dns:_netblocks.google.com", new String[] { "TXT"}).get("TXT").getAll(); + + return super.validateEntity(assetEntity); + } + @Override protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { return identifierPattern; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidator.java index d151b49d..2f4bf5db 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidator.java @@ -15,9 +15,7 @@ class HsEMailAliasHostingAssetValidator extends HsHostingAssetEntityValidator { public static final int EMAIL_ADDRESS_MAX_LENGTH = 320; // according to RFC 5321 and RFC 5322 HsEMailAliasHostingAssetValidator() { - super( BookingItem.mustBeNull(), - ParentAsset.mustBeOfType(HsHostingAssetType.MANAGED_WEBSPACE), - AssignedToAsset.mustBeNull(), + super( HsHostingAssetType.EMAIL_ALIAS, AlarmContact.isOptional(), arrayOf( diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java index 8508ae1e..187630fb 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java @@ -9,7 +9,6 @@ import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; import net.hostsharing.hsadminng.hs.validation.ValidatableProperty; -import jakarta.validation.constraints.NotNull; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -26,21 +25,31 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator[] NO_EXTRA_PROPERTIES = new ValidatableProperty[0]; - private final HsHostingAssetEntityValidator.BookingItem bookingItemValidation; - private final HsHostingAssetEntityValidator.ParentAsset parentAssetValidation; - private final HsHostingAssetEntityValidator.AssignedToAsset assignedToAssetValidation; + private final ReferenceValidator bookingItemReferenceValidation; + private final ReferenceValidator parentAssetReferenceValidation; + private final ReferenceValidator assignedToAssetReferenceValidation; private final HsHostingAssetEntityValidator.AlarmContact alarmContactValidation; HsHostingAssetEntityValidator( - @NotNull final BookingItem bookingItemValidation, - @NotNull final ParentAsset parentAssetValidation, - @NotNull final AssignedToAsset assignedToAssetValidation, - @NotNull final AlarmContact alarmContactValidation, + final HsHostingAssetType assetType, + final AlarmContact alarmContactValidation, final ValidatableProperty... properties) { super(properties); - this.bookingItemValidation = bookingItemValidation; - this.parentAssetValidation = parentAssetValidation; - this.assignedToAssetValidation = assignedToAssetValidation; + this.bookingItemReferenceValidation = new ReferenceValidator<>( + assetType.bookingItemPolicy(), + assetType.bookingItemType(), + HsHostingAssetEntity::getBookingItem, + HsBookingItemEntity::getType); + this.parentAssetReferenceValidation = new ReferenceValidator<>( + assetType.parentAssetPolicy(), + assetType.parentAssetType(), + HsHostingAssetEntity::getParentAsset, + HsHostingAssetEntity::getType); + this.assignedToAssetReferenceValidation = new ReferenceValidator<>( + assetType.assignedToAssetPolicy(), + assetType.assignedToAssetType(), + HsHostingAssetEntity::getAssignedToAsset, + HsHostingAssetEntity::getType); this.alarmContactValidation = alarmContactValidation; } @@ -63,11 +72,11 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator validateEntityReferencesAndProperties(final HsHostingAssetEntity assetEntity) { return Stream.of( - validateReferencedEntity(assetEntity, "bookingItem", bookingItemValidation::validate), - validateReferencedEntity(assetEntity, "parentAsset", parentAssetValidation::validate), - validateReferencedEntity(assetEntity, "assignedToAsset", assignedToAssetValidation::validate), - validateReferencedEntity(assetEntity, "alarmContact", alarmContactValidation::validate), - validateProperties(assetEntity)) + validateReferencedEntity(assetEntity, "bookingItem", bookingItemReferenceValidation::validate), + validateReferencedEntity(assetEntity, "parentAsset", parentAssetReferenceValidation::validate), + validateReferencedEntity(assetEntity, "assignedToAsset", assignedToAssetReferenceValidation::validate), + validateReferencedEntity(assetEntity, "alarmContact", alarmContactValidation::validate), + validateProperties(assetEntity)) .filter(Objects::nonNull) .flatMap(List::stream) .filter(Objects::nonNull) @@ -87,25 +96,28 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator optionallyValidate(final HsHostingAssetEntity assetEntity) { return assetEntity != null - ? enrich(prefix(assetEntity.toShortString(), "parentAsset"), - HsHostingAssetEntityValidatorRegistry.forType(assetEntity.getType()).validateContext(assetEntity)) + ? enrich( + prefix(assetEntity.toShortString(), "parentAsset"), + HsHostingAssetEntityValidatorRegistry.forType(assetEntity.getType()).validateContext(assetEntity)) : emptyList(); } private static List optionallyValidate(final HsBookingItemEntity bookingItem) { return bookingItem != null - ? enrich(prefix(bookingItem.toShortString(), "bookingItem"), - HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem)) + ? enrich( + prefix(bookingItem.toShortString(), "bookingItem"), + HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem)) : emptyList(); } protected List validateAgainstSubEntities(final HsHostingAssetEntity assetEntity) { - return enrich(prefix(assetEntity.toShortString(), "config"), + return enrich( + prefix(assetEntity.toShortString(), "config"), stream(propertyValidators) - .filter(ValidatableProperty::isTotalsValidator) - .map(prop -> validateMaxTotalValue(assetEntity, prop)) - .filter(Objects::nonNull) - .toList()); + .filter(ValidatableProperty::isTotalsValidator) + .map(prop -> validateMaxTotalValue(assetEntity, prop)) + .filter(Objects::nonNull) + .toList()); } // TODO.test: check, if there are any hosting assets which need this validation at all @@ -130,114 +142,79 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator { + static class ReferenceValidator { - private final Policy policy; - private final T subEntityType; - private final Function subEntityGetter; - private final Function subEntityTypeGetter; + private final HsHostingAssetType.RelationPolicy policy; + private final T referencedEntityType; + private final Function referencedEntityGetter; + private final Function referencedEntityTypeGetter; public ReferenceValidator( - final Policy policy, + final HsHostingAssetType.RelationPolicy policy, final T subEntityType, - final Function subEntityGetter, - final Function subEntityTypeGetter) { + final Function referencedEntityGetter, + final Function referencedEntityTypeGetter) { this.policy = policy; - this.subEntityType = subEntityType; - this.subEntityGetter = subEntityGetter; - this.subEntityTypeGetter = subEntityTypeGetter; + this.referencedEntityType = subEntityType; + this.referencedEntityGetter = referencedEntityGetter; + this.referencedEntityTypeGetter = referencedEntityTypeGetter; } public ReferenceValidator( - final Policy policy, - final Function subEntityGetter) { + final HsHostingAssetType.RelationPolicy policy, + final Function referencedEntityGetter) { this.policy = policy; - this.subEntityType = null; - this.subEntityGetter = subEntityGetter; - this.subEntityTypeGetter = e -> null; - } - - enum Policy { - OPTIONAL, FORBIDDEN, REQUIRED + this.referencedEntityType = null; + this.referencedEntityGetter = referencedEntityGetter; + this.referencedEntityTypeGetter = e -> null; } List validate(final HsHostingAssetEntity assetEntity, final String referenceFieldName) { - final var subEntity = subEntityGetter.apply(assetEntity); - if (policy == Policy.REQUIRED && subEntity == null) { - return List.of(referenceFieldName + "' must not be null but is null"); - } - if (policy == Policy.FORBIDDEN && subEntity != null) { - return List.of(referenceFieldName + "' must be null but is set to "+ assetEntity.getBookingItem().toShortString()); - } - final var subItemType = subEntity != null ? subEntityTypeGetter.apply(subEntity) : null; - if (subEntityType != null && subItemType != subEntityType) { - return List.of(referenceFieldName + "' must be of type " + subEntityType + " but is of type " + subItemType); + final var actualEntity = referencedEntityGetter.apply(assetEntity); + final var actualEntityType = actualEntity != null ? referencedEntityTypeGetter.apply(actualEntity) : null; + + switch (policy) { + case REQUIRED: + if (actualEntityType != referencedEntityType) { + return List.of(actualEntityType == null + ? referenceFieldName + "' must be of type " + referencedEntityType + " but is null" + : referenceFieldName + "' must be of type " + referencedEntityType + " but is of type " + actualEntityType); + } + break; + case OPTIONAL: + if (actualEntityType != null && actualEntityType != referencedEntityType) { + return List.of(referenceFieldName + "' must be null or of type " + referencedEntityType + " but is of type " + + actualEntityType); + } + break; + case FORBIDDEN: + if (actualEntityType != null) { + return List.of(referenceFieldName + "' must be null but is of type " + actualEntityType); + } + break; } return emptyList(); } } - static class BookingItem extends ReferenceValidator { - - BookingItem(final Policy policy, final HsBookingItemType bookingItemType) { - super(policy, bookingItemType, HsHostingAssetEntity::getBookingItem, HsBookingItemEntity::getType); - } - - static BookingItem mustBeNull() { - return new BookingItem(Policy.FORBIDDEN, null); - } - - static BookingItem mustBeOfType(final HsBookingItemType hsBookingItemType) { - return new BookingItem(Policy.REQUIRED, hsBookingItemType); - } - } - - static class ParentAsset extends ReferenceValidator { - - ParentAsset(final ReferenceValidator.Policy policy, final HsHostingAssetType parentAssetType) { - super(policy, parentAssetType, HsHostingAssetEntity::getParentAsset, HsHostingAssetEntity::getType); - } - - static ParentAsset mustBeNull() { - return new ParentAsset(Policy.FORBIDDEN, null); - } - - static ParentAsset mustBeOfType(final HsHostingAssetType hostingAssetType) { - return new ParentAsset(Policy.REQUIRED, hostingAssetType); - } - - static ParentAsset mustBeNullOrOfType(final HsHostingAssetType hostingAssetType) { - return new ParentAsset(Policy.OPTIONAL, hostingAssetType); - } - } - - static class AssignedToAsset extends ReferenceValidator { - - AssignedToAsset(final ReferenceValidator.Policy policy, final HsHostingAssetType assignedToAssetType) { - super(policy, assignedToAssetType, HsHostingAssetEntity::getAssignedToAsset, HsHostingAssetEntity::getType); - } - - static AssignedToAsset mustBeNull() { - return new AssignedToAsset(Policy.FORBIDDEN, null); - } - } - static class AlarmContact extends ReferenceValidator> { - AlarmContact(final ReferenceValidator.Policy policy) { + AlarmContact(final HsHostingAssetType.RelationPolicy policy) { super(policy, HsHostingAssetEntity::getAlarmContact); } static AlarmContact isOptional() { - return new AlarmContact(Policy.OPTIONAL); + return new AlarmContact(HsHostingAssetType.RelationPolicy.OPTIONAL); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java index 362abf38..69c0efff 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java @@ -1,10 +1,10 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; import java.util.regex.Pattern; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty; import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty; import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; @@ -13,9 +13,7 @@ class HsManagedServerHostingAssetValidator extends HsHostingAssetEntityValidator public HsManagedServerHostingAssetValidator() { super( - BookingItem.mustBeOfType(HsBookingItemType.MANAGED_SERVER), - ParentAsset.mustBeNull(), // until we introduce a hosting asset for 'HOST' - AssignedToAsset.mustBeNull(), + MANAGED_SERVER, AlarmContact.isOptional(), // hostmaster alert address is implicitly added // monitoring 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 bffedf2f..443aea02 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 @@ -1,16 +1,15 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; -import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; -import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import java.util.regex.Pattern; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; + class HsManagedWebspaceHostingAssetValidator extends HsHostingAssetEntityValidator { public HsManagedWebspaceHostingAssetValidator() { - super(BookingItem.mustBeOfType(HsBookingItemType.MANAGED_WEBSPACE), - ParentAsset.mustBeOfType(HsHostingAssetType.MANAGED_SERVER), // the (shared or private) ManagedServer - AssignedToAsset.mustBeNull(), + super( + MANAGED_WEBSPACE, AlarmContact.isOptional(), // hostmaster alert address is implicitly added NO_EXTRA_PROPERTIES); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java index 309404f6..579c3134 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java @@ -17,9 +17,8 @@ class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator { private static final int DASH_LENGTH = "-".length(); HsUnixUserHostingAssetValidator() { - super( BookingItem.mustBeNull(), - ParentAsset.mustBeOfType(HsHostingAssetType.MANAGED_WEBSPACE), - AssignedToAsset.mustBeNull(), + super( + HsHostingAssetType.UNIX_USER, AlarmContact.isOptional(), integerProperty("SSD hard quota").unit("GB").maxFrom("SSD").optional(), diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java index de4b70bc..fac624cf 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java @@ -1,6 +1,7 @@ package net.hostsharing.hsadminng.hs.validation; + import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -18,7 +19,7 @@ public abstract class HsEntityValidator { public final ValidatableProperty[] propertyValidators; - public HsEntityValidator(final ValidatableProperty... validators) { + public > HsEntityValidator(final ValidatableProperty... validators) { propertyValidators = validators; stream(propertyValidators).forEach(p -> p.deferredInit(propertyValidators)); } diff --git a/src/main/resources/db/changelog/0-basis/010-context.sql b/src/main/resources/db/changelog/0-basis/010-context.sql index 8ea73f45..25c6c48c 100644 --- a/src/main/resources/db/changelog/0-basis/010-context.sql +++ b/src/main/resources/db/changelog/0-basis/010-context.sql @@ -149,7 +149,7 @@ create or replace function cleanIdentifier(rawIdentifier varchar) declare cleanIdentifier varchar; begin - cleanIdentifier := regexp_replace(rawIdentifier, '[^A-Za-z0-9\-._]+', '', 'g'); + cleanIdentifier := regexp_replace(rawIdentifier, '[^A-Za-z0-9\-._|]+', '', 'g'); return cleanIdentifier; end; $$; 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 37b47e15..019bb0a2 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 @@ -36,9 +36,9 @@ subgraph asset["`**asset**`"] style asset:permissions fill:#dd4901,stroke:white perm:asset:INSERT{{asset:INSERT}} - perm:asset:SELECT{{asset:SELECT}} perm:asset:DELETE{{asset:DELETE}} perm:asset:UPDATE{{asset:UPDATE}} + perm:asset:SELECT{{asset:SELECT}} end end @@ -80,6 +80,9 @@ subgraph parentAsset["`**parentAsset**`"] end end +%% granting roles to users +user:creator ==> role:asset:OWNER + %% granting roles to roles role:bookingItem:OWNER -.-> role:bookingItem:ADMIN role:bookingItem:ADMIN -.-> role:bookingItem:AGENT @@ -87,6 +90,7 @@ role:bookingItem:AGENT -.-> role:bookingItem:TENANT role:global:ADMIN -.-> role:alarmContact:OWNER role:alarmContact:OWNER -.-> role:alarmContact:ADMIN role:alarmContact:ADMIN -.-> role:alarmContact:REFERRER +role:global:ADMIN ==>|XX| role:asset:OWNER role:bookingItem:ADMIN ==> role:asset:OWNER role:parentAsset:ADMIN ==> role:asset:OWNER role:asset:OWNER ==> role:asset:ADMIN @@ -104,7 +108,6 @@ role:alarmContact:ADMIN ==> role:asset:TENANT role:global:ADMIN ==> perm:asset:INSERT role:parentAsset:ADMIN ==> perm:asset:INSERT role:global:GUEST ==> perm:asset:INSERT -role:global:ADMIN ==> perm:asset:SELECT 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 5b740226..91afe2b6 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 @@ -50,8 +50,10 @@ begin hsHostingAssetOWNER(NEW), permissions => array['DELETE'], incomingSuperRoles => array[ + globalADMIN(unassumed()), hsBookingItemADMIN(newBookingItem), - hsHostingAssetADMIN(newParentAsset)] + hsHostingAssetADMIN(newParentAsset)], + userUuids => array[currentUserUuid()] ); perform createRoleWithGrants( @@ -85,10 +87,6 @@ begin IF NEW.type = 'DOMAIN_SETUP' THEN END IF; - - - call grantPermissionToRole(createPermission(NEW.uuid, 'SELECT'), globalAdmin()); - call leaveTriggerForObjectUuid(NEW.uuid); end; $$; 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 736c129d..26ef2ac8 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 @@ -71,15 +71,15 @@ begin defaultPrefix := relatedDebitor.defaultPrefix; insert into hs_hosting_asset - (uuid, bookingitemuuid, type, parentAssetUuid, assignedToAssetUuid, identifier, caption, config) - values (managedServerUuid, relatedManagedServerBookingItem.uuid, 'MANAGED_SERVER', null, null, 'vm10' || debitorNumberSuffix, 'some ManagedServer', '{ "monit_max_cpu_usage": 90, "monit_max_ram_usage": 80, "monit_max_ssd_usage": 70 }'::jsonb), - (uuid_generate_v4(), relatedCloudServerBookingItem.uuid, 'CLOUD_SERVER', null, null, 'vm20' || debitorNumberSuffix, 'another CloudServer', '{}'::jsonb), - (managedWebspaceUuid, relatedManagedWebspaceBookingItem.uuid, 'MANAGED_WEBSPACE', managedServerUuid, null, defaultPrefix || '01', 'some Webspace', '{}'::jsonb), - (uuid_generate_v4(), null, 'EMAIL_ALIAS', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some E-Mail-Alias', '{ "target": [ "office@example.org", "archive@example.com" ] }'::jsonb), - (webUnixUserUuid, null, 'UNIX_USER', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some UnixUser for Website', '{ "SSD-soft-quota": "128", "SSD-hard-quota": "256", "HDD-soft-quota": "512", "HDD-hard-quota": "1024"}'::jsonb), - (domainSetupUuid, null, 'DOMAIN_SETUP', null, null, defaultPrefix || '.example.org', 'some Domain-Setup', '{}'::jsonb), - (uuid_generate_v4(), null, 'DOMAIN_DNS_SETUP', domainSetupUuid, null, defaultPrefix || '.example.org', 'some Domain-DNS-Setup', '{}'::jsonb), - (uuid_generate_v4(), null, 'DOMAIN_HTTP_SETUP', domainSetupUuid, webUnixUserUuid, defaultPrefix || '.example.org', 'some Domain-HTTP-Setup', '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*"}'::jsonb); + (uuid, bookingitemuuid, type, parentAssetUuid, assignedToAssetUuid, identifier, caption, config) + values (managedServerUuid, relatedManagedServerBookingItem.uuid, 'MANAGED_SERVER', null, null, 'vm10' || debitorNumberSuffix, 'some ManagedServer', '{ "monit_max_cpu_usage": 90, "monit_max_ram_usage": 80, "monit_max_ssd_usage": 70 }'::jsonb), + (uuid_generate_v4(), relatedCloudServerBookingItem.uuid, 'CLOUD_SERVER', null, null, 'vm20' || debitorNumberSuffix, 'another CloudServer', '{}'::jsonb), + (managedWebspaceUuid, relatedManagedWebspaceBookingItem.uuid, 'MANAGED_WEBSPACE', managedServerUuid, null, defaultPrefix || '01', 'some Webspace', '{}'::jsonb), + (uuid_generate_v4(), null, 'EMAIL_ALIAS', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some E-Mail-Alias', '{ "target": [ "office@example.org", "archive@example.com" ] }'::jsonb), + (webUnixUserUuid, null, 'UNIX_USER', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some UnixUser for Website', '{ "SSD-soft-quota": "128", "SSD-hard-quota": "256", "HDD-soft-quota": "512", "HDD-hard-quota": "1024"}'::jsonb), + (domainSetupUuid, null, 'DOMAIN_SETUP', null, null, defaultPrefix || '.example.org', 'some Domain-Setup', '{}'::jsonb), + (uuid_generate_v4(), null, 'DOMAIN_DNS_SETUP', domainSetupUuid, null, defaultPrefix || '.example.org|DNS', 'some Domain-DNS-Setup', '{}'::jsonb), + (uuid_generate_v4(), null, 'DOMAIN_HTTP_SETUP', domainSetupUuid, webUnixUserUuid, defaultPrefix || '.example.org|HTTP', 'some Domain-HTTP-Setup', '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*"}'::jsonb); end; $$; --// diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index f626a3ed..cc2dafa6 100644 --- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java +++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java @@ -40,8 +40,10 @@ public class ArchitectureTest { "..test.pac", "..test.dom", "..context", + "..hash", "..generated..", "..persistence..", + "..system..", "..validation..", "..hs.office.bankaccount", "..hs.office.contact", @@ -110,6 +112,13 @@ public class ArchitectureTest { .should().onlyDependOnClassesThat() .resideOutsideOfPackage(NET_HOSTSHARING_HSADMINNG); + @ArchTest + @SuppressWarnings("unused") + public static final ArchRule hashPackageRule = classes() + .that().resideInAPackage("..hash..") + .should().onlyDependOnClassesThat() + .resideOutsideOfPackage(NET_HOSTSHARING_HSADMINNG); + @ArchTest @SuppressWarnings("unused") public static final ArchRule errorsPackageRule = classes() @@ -117,6 +126,13 @@ public class ArchitectureTest { .should().onlyDependOnClassesThat() .resideOutsideOfPackage(NET_HOSTSHARING_HSADMINNG); + @ArchTest + @SuppressWarnings("unused") + public static final ArchRule systemPackageRule = classes() + .that().resideInAPackage("..system..") + .should().onlyDependOnClassesThat() + .resideOutsideOfPackage(NET_HOSTSHARING_HSADMINNG); + @ArchTest @SuppressWarnings("unused") public static final ArchRule testPackagesRule = classes() diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java index 11020d92..b0605239 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java @@ -152,7 +152,7 @@ class HsManagedServerBookingItemValidatorUnitTest { "xyz00_%c%c", 2, HsHostingAssetType.MARIADB_DATABASE ), - generateDomainEmailSetupsWithEMailAddresses(26, HsHostingAssetType.DOMAIN_EMAIL_SETUP, + generateDomainEmailSetupsWithEMailAddresses(26, HsHostingAssetType.DOMAIN_EMAIL_MAILBOX_SETUP, "%c%c.example.com", 10, HsHostingAssetType.EMAIL_ADDRESS ) 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 20ebd989..e45a157b 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 @@ -235,6 +235,47 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup assertThat(newUserUuid).isNotNull(); } + @Test + void globalAdmin_canAddTopLevelAsset() { + + context.define("superuser-alex@hostsharing.net"); + + final var location = RestAssured // @formatter:off + .given() + .header("current-user", "superuser-alex@hostsharing.net") + .contentType(ContentType.JSON) + .body(""" + { + "type": "DOMAIN_SETUP", + "identifier": "example.com", + "caption": "some unrelated domain-setup", + "config": {} + } + """) + .port(port) + .when() + .post("http://localhost/api/hs/hosting/assets") + .then().log().all().assertThat() + .statusCode(201) + .contentType(ContentType.JSON) + .body("", lenientlyEquals(""" + { + "type": "DOMAIN_SETUP", + "identifier": "example.com", + "caption": "some unrelated domain-setup", + "config": {} + } + """)) + .header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*")) + .extract().header("Location"); // @formatter:on + + // finally, the new asset can be accessed under the generated UUID + final var newWebspace = UUID.fromString( + location.substring(location.lastIndexOf('/') + 1)); + assertThat(newWebspace).isNotNull(); + toCleanup(HsHostingAssetEntity.class, newWebspace); + } + @Test void propertyValidationsArePerformend_whenAddingAsset() { 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 579257a0..40f38d7b 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 @@ -131,9 +131,10 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu initialGrantNames, // global-admin - "{ grant perm:hs_hosting_asset#fir00:SELECT to role:global#global:ADMIN by system and assume }", // workaround + "{ grant role:hs_hosting_asset#fir00:OWNER to role:global#global:ADMIN by system }", // workaround // owner + "{ grant role:hs_hosting_asset#fir00:OWNER to user:superuser-alex@hostsharing.net by hs_hosting_asset#fir00:OWNER and assume }", "{ grant role:hs_hosting_asset#fir00:OWNER to role:hs_booking_item#fir01:ADMIN by system and assume }", "{ grant role:hs_hosting_asset#fir00:OWNER to role:hs_hosting_asset#vm1011:ADMIN by system and assume }", "{ grant perm:hs_hosting_asset#fir00:DELETE to role:hs_hosting_asset#fir00:OWNER by system and assume }", @@ -158,37 +159,38 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu @Test public void anyUser_canCreateNewDomainSetupAsset() { - // given - context("superuser-alex@hostsharing.net"); - final var assetCount = assetRepo.count(); - // when context("person-SmithPeter@example.com"); final var result = attempt(em, () -> { final var newAsset = HsHostingAssetEntity.builder() - .caption("some new domain setup") .type(DOMAIN_SETUP) - .identifier("example.org") + .identifier("example.net") + .caption("some new domain setup") .build(); - return toCleanup(assetRepo.save(newAsset)); + return assetRepo.save(newAsset); }); // then + // ... the domain setup was created and returned result.assertSuccessful(); assertThat(result.returnedValue()).isNotNull().extracting(HsHostingAssetEntity::getUuid).isNotNull(); assertThat(result.returnedValue().isLoaded()).isFalse(); - context("superuser-alex@hostsharing.net"); + + // ... the creating user can read the new domain setup + context("person-SmithPeter@example.com"); + assertThatAssetIsPersisted(result.returnedValue()); + + // ... a global admin can see the new domain setup as well if the domain OWNER role is assumed + context("superuser-alex@hostsharing.net", "hs_hosting_asset#example.net:OWNER"); // only works with the assumed role assertThatAssetIsPersisted(result.returnedValue()); - assertThat(assetRepo.count()).isEqualTo(assetCount + 1); } private void assertThatAssetIsPersisted(final HsHostingAssetEntity saved) { - final var context = + em.clear(); attempt(em, () -> { final var found = assetRepo.findByUuid(saved.getUuid()); - assertThat(found).isNotEmpty().map(HsHostingAssetEntity::toString).get().isEqualTo(saved.toString()); + assertThat(found).isNotEmpty().map(HsHostingAssetEntity::toString).contains(saved.toString()); }); - } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetTypeUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetTypeUnitTest.java new file mode 100644 index 00000000..794c3f25 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetTypeUnitTest.java @@ -0,0 +1,219 @@ +package net.hostsharing.hsadminng.hs.hosting.asset; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class HsHostingAssetTypeUnitTest { + + @Test + void generatedPlantUML() { + final var result = HsHostingAssetType.renderAsEmbeddedPlantUml(); + + assertThat(result).isEqualTo(""" + ## HostingAsset Type Structure + + + ### Domain + + ```plantuml + @startuml + left to right direction + + package Booking #feb28c { + entity BI_PRIVATE_CLOUD + entity BI_CLOUD_SERVER + entity BI_MANAGED_SERVER + entity BI_MANAGED_WEBSPACE + entity BI_DOMAIN_DNS_SETUP + entity BI_DOMAIN_EMAIL_SUBMISSION_SETUP + } + + package Hosting #feb28c{ + package Domain #99bcdb { + entity HA_DOMAIN_SETUP + entity HA_DOMAIN_DNS_SETUP + entity HA_DOMAIN_HTTP_SETUP + entity HA_DOMAIN_EMAIL_SUBMISSION_SETUP + entity HA_DOMAIN_EMAIL_MAILBOX_SETUP + entity HA_EMAIL_ADDRESS + } + + package Server #99bcdb { + entity HA_CLOUD_SERVER + entity HA_MANAGED_SERVER + entity HA_IP_NUMBER + } + + package Webspace #99bcdb { + entity HA_MANAGED_WEBSPACE + entity HA_UNIX_USER + entity HA_EMAIL_ALIAS + } + + } + + BI_CLOUD_SERVER *--> BI_PRIVATE_CLOUD + BI_MANAGED_SERVER *--> BI_PRIVATE_CLOUD + BI_MANAGED_WEBSPACE *--> BI_MANAGED_SERVER + + HA_CLOUD_SERVER *==> BI_CLOUD_SERVER + HA_MANAGED_SERVER *==> BI_MANAGED_SERVER + HA_MANAGED_WEBSPACE *==> BI_MANAGED_WEBSPACE + HA_MANAGED_WEBSPACE o..> HA_MANAGED_SERVER + HA_UNIX_USER *==> HA_MANAGED_WEBSPACE + HA_DOMAIN_SETUP o..> HA_DOMAIN_SETUP + HA_DOMAIN_DNS_SETUP *==> HA_DOMAIN_SETUP + HA_DOMAIN_HTTP_SETUP *==> HA_DOMAIN_SETUP + HA_DOMAIN_HTTP_SETUP o..> HA_UNIX_USER + HA_DOMAIN_EMAIL_SUBMISSION_SETUP *==> HA_DOMAIN_SETUP + HA_DOMAIN_EMAIL_SUBMISSION_SETUP o..> HA_MANAGED_WEBSPACE + HA_DOMAIN_EMAIL_MAILBOX_SETUP *==> HA_DOMAIN_SETUP + HA_DOMAIN_EMAIL_MAILBOX_SETUP o..> HA_MANAGED_WEBSPACE + HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE + HA_EMAIL_ADDRESS *==> HA_DOMAIN_EMAIL_MAILBOX_SETUP + HA_IP_NUMBER o..> HA_CLOUD_SERVER + HA_IP_NUMBER o..> HA_MANAGED_SERVER + HA_IP_NUMBER o..> HA_MANAGED_WEBSPACE + + package Legend #white { + SUB_ENTITY1 *--> REQUIRED_PARENT_ENTITY + SUB_ENTITY2 *..> OPTIONAL_PARENT_ENTITY + ASSIGNED_ENTITY1 o--> REQUIRED_ASSIGNED_TO_ENTITY1 + ASSIGNED_ENTITY2 o..> OPTIONAL_ASSIGNED_TO_ENTITY2 + } + Booking -down[hidden]->Legend + ``` + + ### MariaDB + + ```plantuml + @startuml + left to right direction + + package Booking #feb28c { + entity BI_PRIVATE_CLOUD + entity BI_CLOUD_SERVER + entity BI_MANAGED_SERVER + entity BI_MANAGED_WEBSPACE + entity BI_DOMAIN_DNS_SETUP + entity BI_DOMAIN_EMAIL_SUBMISSION_SETUP + } + + package Hosting #feb28c{ + package MariaDB #99bcdb { + entity HA_MARIADB_INSTANCE + entity HA_MARIADB_USER + entity HA_MARIADB_DATABASE + } + + package Server #99bcdb { + entity HA_CLOUD_SERVER + entity HA_MANAGED_SERVER + entity HA_IP_NUMBER + } + + package Webspace #99bcdb { + entity HA_MANAGED_WEBSPACE + entity HA_UNIX_USER + entity HA_EMAIL_ALIAS + } + + } + + BI_CLOUD_SERVER *--> BI_PRIVATE_CLOUD + BI_MANAGED_SERVER *--> BI_PRIVATE_CLOUD + BI_MANAGED_WEBSPACE *--> BI_MANAGED_SERVER + + HA_CLOUD_SERVER *==> BI_CLOUD_SERVER + HA_MANAGED_SERVER *==> BI_MANAGED_SERVER + HA_MANAGED_WEBSPACE *==> BI_MANAGED_WEBSPACE + HA_MANAGED_WEBSPACE o..> HA_MANAGED_SERVER + HA_UNIX_USER *==> HA_MANAGED_WEBSPACE + HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE + HA_MARIADB_INSTANCE *==> HA_MANAGED_SERVER + HA_MARIADB_USER *==> HA_MARIADB_INSTANCE + HA_MARIADB_USER o..> HA_MANAGED_WEBSPACE + HA_MARIADB_DATABASE *==> HA_MANAGED_WEBSPACE + HA_MARIADB_DATABASE o..> HA_MARIADB_INSTANCE + HA_IP_NUMBER o..> HA_CLOUD_SERVER + HA_IP_NUMBER o..> HA_MANAGED_SERVER + HA_IP_NUMBER o..> HA_MANAGED_WEBSPACE + + package Legend #white { + SUB_ENTITY1 *--> REQUIRED_PARENT_ENTITY + SUB_ENTITY2 *..> OPTIONAL_PARENT_ENTITY + ASSIGNED_ENTITY1 o--> REQUIRED_ASSIGNED_TO_ENTITY1 + ASSIGNED_ENTITY2 o..> OPTIONAL_ASSIGNED_TO_ENTITY2 + } + Booking -down[hidden]->Legend + ``` + + ### PostgreSQL + + ```plantuml + @startuml + left to right direction + + package Booking #feb28c { + entity BI_PRIVATE_CLOUD + entity BI_CLOUD_SERVER + entity BI_MANAGED_SERVER + entity BI_MANAGED_WEBSPACE + entity BI_DOMAIN_DNS_SETUP + entity BI_DOMAIN_EMAIL_SUBMISSION_SETUP + } + + package Hosting #feb28c{ + package PostgreSQL #99bcdb { + entity HA_PGSQL_INSTANCE + entity HA_PGSQL_USER + entity HA_PGSQL_DATABASE + } + + package Server #99bcdb { + entity HA_CLOUD_SERVER + entity HA_MANAGED_SERVER + entity HA_IP_NUMBER + } + + package Webspace #99bcdb { + entity HA_MANAGED_WEBSPACE + entity HA_UNIX_USER + entity HA_EMAIL_ALIAS + } + + } + + BI_CLOUD_SERVER *--> BI_PRIVATE_CLOUD + BI_MANAGED_SERVER *--> BI_PRIVATE_CLOUD + BI_MANAGED_WEBSPACE *--> BI_MANAGED_SERVER + + HA_CLOUD_SERVER *==> BI_CLOUD_SERVER + HA_MANAGED_SERVER *==> BI_MANAGED_SERVER + HA_MANAGED_WEBSPACE *==> BI_MANAGED_WEBSPACE + HA_MANAGED_WEBSPACE o..> HA_MANAGED_SERVER + HA_UNIX_USER *==> HA_MANAGED_WEBSPACE + HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE + HA_PGSQL_INSTANCE *==> HA_MANAGED_SERVER + HA_PGSQL_USER *==> HA_PGSQL_INSTANCE + HA_PGSQL_USER o..> HA_MANAGED_WEBSPACE + HA_PGSQL_DATABASE *==> HA_MANAGED_WEBSPACE + HA_PGSQL_DATABASE o..> HA_PGSQL_INSTANCE + HA_IP_NUMBER o..> HA_CLOUD_SERVER + HA_IP_NUMBER o..> HA_MANAGED_SERVER + HA_IP_NUMBER o..> HA_MANAGED_WEBSPACE + + package Legend #white { + SUB_ENTITY1 *--> REQUIRED_PARENT_ENTITY + SUB_ENTITY2 *..> OPTIONAL_PARENT_ENTITY + ASSIGNED_ENTITY1 o--> REQUIRED_ASSIGNED_TO_ENTITY1 + ASSIGNED_ENTITY2 o..> OPTIONAL_ASSIGNED_TO_ENTITY2 + } + Booking -down[hidden]->Legend + ``` + + This code generated was by HsHostingAssetType.main, do not amend manually. + """); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java index 69fe01bb..8361c5f4 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java @@ -33,7 +33,7 @@ class HsCloudServerHostingAssetValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( - "'CLOUD_SERVER:vm1234.bookingItem' must not be null but is null", + "'CLOUD_SERVER:vm1234.bookingItem' must be of type CLOUD_SERVER but is null", "'CLOUD_SERVER:vm1234.config.RAM' is not expected but is set to '2000'"); } @@ -84,14 +84,14 @@ class HsCloudServerHostingAssetValidatorUnitTest { } @Test - void validatesParentAndAssignedToAssetMustNotBeSet() { + void rejectsInvalidReferencedEntities() { // given final var mangedServerHostingAssetEntity = HsHostingAssetEntity.builder() .type(CLOUD_SERVER) - .identifier("xyz00") - .parentAsset(HsHostingAssetEntity.builder().build()) - .assignedToAsset(HsHostingAssetEntity.builder().build()) + .identifier("vm1234") .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build()) + .parentAsset(HsHostingAssetEntity.builder().type(MANAGED_SERVER).build()) + .assignedToAsset(HsHostingAssetEntity.builder().type(CLOUD_SERVER).build()) .build(); final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); @@ -100,7 +100,7 @@ class HsCloudServerHostingAssetValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( - "'CLOUD_SERVER:xyz00.parentAsset' must be null but is set to D-???????-?:null", - "'CLOUD_SERVER:xyz00.assignedToAsset' must be null but is set to D-???????-?:null"); + "'CLOUD_SERVER:vm1234.parentAsset' must be null but is of type MANAGED_SERVER", + "'CLOUD_SERVER:vm1234.assignedToAsset' must be null but is of type CLOUD_SERVER"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java index 671b9452..681196ae 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java @@ -31,7 +31,7 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { return HsHostingAssetEntity.builder() .type(DOMAIN_DNS_SETUP) .parentAsset(validDomainSetupEntity) - .identifier("example.org") + .identifier("example.org|DNS") .config(Map.ofEntries( entry("user-RR", Array.of( "@ 1814400 IN XXX example.org. root.example.org ( 1234 10800 900 604800 86400 )", @@ -74,19 +74,20 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { void preprocessesTakesIdentifierFromParent() { // given final var givenEntity = validEntityBuilder().build(); + assertThat(givenEntity.getParentAsset().getIdentifier()).as("preconditon failed").isEqualTo("example.org"); final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); // when validator.preprocessEntity(givenEntity); // then - assertThat(givenEntity.getIdentifier()).isEqualTo(givenEntity.getParentAsset().getIdentifier()); + assertThat(givenEntity.getIdentifier()).isEqualTo("example.org|DNS"); } @Test void rejectsInvalidIdentifier() { // given - final var givenEntity = validEntityBuilder().identifier("wrong.org").build(); + final var givenEntity = validEntityBuilder().identifier("example.org").build(); final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); // when @@ -94,14 +95,14 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { // then assertThat(result).containsExactly( - "'identifier' expected to match '^example.org$', but is 'wrong.org'" + "'identifier' expected to match '^example.org\\Q|DNS\\E$', but is 'example.org'" ); } @Test void acceptsValidIdentifier() { // given - final var givenEntity = validEntityBuilder().identifier(validDomainSetupEntity.getIdentifier()).build(); + final var givenEntity = validEntityBuilder().identifier(validDomainSetupEntity.getIdentifier()+"|DNS").build(); final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); // when @@ -112,12 +113,12 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { } @Test - void validatesReferencedEntities() { + void rejectsInvalidReferencedEntities() { // given final var mangedServerHostingAssetEntity = validEntityBuilder() - .parentAsset(HsHostingAssetEntity.builder().build()) - .assignedToAsset(HsHostingAssetEntity.builder().build()) .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build()) + .parentAsset(null) + .assignedToAsset(HsHostingAssetEntity.builder().type(DOMAIN_SETUP).build()) .build(); final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); @@ -126,9 +127,9 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( - "'DOMAIN_DNS_SETUP:example.org.bookingItem' must be null but is set to D-???????-?:null", - "'DOMAIN_DNS_SETUP:example.org.parentAsset' must be of type DOMAIN_SETUP but is of type null", - "'DOMAIN_DNS_SETUP:example.org.assignedToAsset' must be null but is set to D-???????-?:null"); + "'DOMAIN_DNS_SETUP:example.org|DNS.bookingItem' must be null but is of type CLOUD_SERVER", + "'DOMAIN_DNS_SETUP:example.org|DNS.parentAsset' must be of type DOMAIN_SETUP but is null", + "'DOMAIN_DNS_SETUP:example.org|DNS.assignedToAsset' must be null but is of type DOMAIN_SETUP"); } @Test @@ -162,9 +163,9 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( - "'DOMAIN_DNS_SETUP:example.org.config.TTL' is expected to be of type class java.lang.Integer, but is of type 'String'", - "'DOMAIN_DNS_SETUP:example.org.config.user-RR' is expected to match any of [([a-z0-9\\.-]+|@)\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*IN\\s+[A-Z]+\\s+[^;].*(;.*)*, ([a-z0-9\\.-]+|@)\\s+IN\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*[A-Z]+\\s+[^;].*(;.*)*] but '@ 1814400 IN 1814400 BAD1 TTL only allowed once' does not match any", - "'DOMAIN_DNS_SETUP:example.org.config.user-RR' is expected to match any of [([a-z0-9\\.-]+|@)\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*IN\\s+[A-Z]+\\s+[^;].*(;.*)*, ([a-z0-9\\.-]+|@)\\s+IN\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*[A-Z]+\\s+[^;].*(;.*)*] but 'www BAD1 Record-Class missing / not enough columns' does not match any"); + "'DOMAIN_DNS_SETUP:example.org|DNS.config.TTL' is expected to be of type class java.lang.Integer, but is of type 'String'", + "'DOMAIN_DNS_SETUP:example.org|DNS.config.user-RR' is expected to match any of [([a-z0-9\\.-]+|@)\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*IN\\s+[A-Z]+\\s+[^;].*(;.*)*, ([a-z0-9\\.-]+|@)\\s+IN\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*[A-Z]+\\s+[^;].*(;.*)*] but '@ 1814400 IN 1814400 BAD1 TTL only allowed once' does not match any", + "'DOMAIN_DNS_SETUP:example.org|DNS.config.user-RR' is expected to match any of [([a-z0-9\\.-]+|@)\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*IN\\s+[A-Z]+\\s+[^;].*(;.*)*, ([a-z0-9\\.-]+|@)\\s+IN\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*[A-Z]+\\s+[^;].*(;.*)*] but 'www BAD1 Record-Class missing / not enough columns' does not match any"); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java index b7d78567..c9b99784 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java @@ -12,6 +12,7 @@ import java.util.Map; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; import static org.assertj.core.api.Assertions.assertThat; class HsDomainSetupHostingAssetValidatorUnitTest { @@ -93,8 +94,8 @@ class HsDomainSetupHostingAssetValidatorUnitTest { void validatesReferencedEntities() { // given final var mangedServerHostingAssetEntity = validEntityBuilder() - .parentAsset(HsHostingAssetEntity.builder().build()) - .assignedToAsset(HsHostingAssetEntity.builder().build()) + .parentAsset(HsHostingAssetEntity.builder().type(CLOUD_SERVER).build()) + .assignedToAsset(HsHostingAssetEntity.builder().type(MANAGED_SERVER).build()) .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build()) .build(); final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); @@ -104,8 +105,8 @@ class HsDomainSetupHostingAssetValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( - "'DOMAIN_SETUP:example.org.bookingItem' must be null but is set to D-???????-?:null", - "'DOMAIN_SETUP:example.org.parentAsset' must be null but is set to D-???????-?:null", - "'DOMAIN_SETUP:example.org.assignedToAsset' must be null but is set to D-???????-?:null"); + "'DOMAIN_SETUP:example.org.bookingItem' must be null but is of type CLOUD_SERVER", + "'DOMAIN_SETUP:example.org.parentAsset' must be null or of type DOMAIN_SETUP but is of type CLOUD_SERVER", + "'DOMAIN_SETUP:example.org.assignedToAsset' must be null but is of type MANAGED_SERVER"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java index 6c35078b..38e7564e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAliasHostingAssetValidatorUnitTest.java @@ -107,8 +107,8 @@ class HsEMailAliasHostingAssetValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( - "'EMAIL_ALIAS:abc00-office.bookingItem' must be null but is set to D-1234500:test project:test project booking item", + "'EMAIL_ALIAS:abc00-office.bookingItem' must be null but is of type MANAGED_SERVER", "'EMAIL_ALIAS:abc00-office.parentAsset' must be of type MANAGED_WEBSPACE but is of type MANAGED_SERVER", - "'EMAIL_ALIAS:abc00-office.assignedToAsset' must be null but is set to D-1234500:test project:test project booking item"); + "'EMAIL_ALIAS:abc00-office.assignedToAsset' must be null but is of type MANAGED_SERVER"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java index fd8d4800..15b2ca97 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java @@ -10,6 +10,7 @@ import java.util.Map; import static java.util.Map.entry; import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_CLOUD_SERVER_BOOKING_ITEM; import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_SERVER_BOOKING_ITEM; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; import static org.assertj.core.api.Assertions.assertThat; @@ -22,8 +23,8 @@ class HsManagedServerHostingAssetValidatorUnitTest { .type(MANAGED_SERVER) .identifier("vm1234") .bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM) - .parentAsset(HsHostingAssetEntity.builder().build()) - .assignedToAsset(HsHostingAssetEntity.builder().build()) + .parentAsset(HsHostingAssetEntity.builder().type(CLOUD_SERVER).build()) + .assignedToAsset(HsHostingAssetEntity.builder().type(CLOUD_SERVER).build()) .config(Map.ofEntries( entry("monit_max_hdd_usage", "90"), entry("monit_max_cpu_usage", 2), @@ -37,8 +38,8 @@ class HsManagedServerHostingAssetValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( - "'MANAGED_SERVER:vm1234.parentAsset' must be null but is set to D-1234500:test project:test project booking item", - "'MANAGED_SERVER:vm1234.assignedToAsset' must be null but is set to D-1234500:test project:test project booking item", + "'MANAGED_SERVER:vm1234.parentAsset' must be null but is of type CLOUD_SERVER", + "'MANAGED_SERVER:vm1234.assignedToAsset' must be null but is of type CLOUD_SERVER", "'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be at least 10 but is 2", "'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be at most 100 but is 101", "'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'"); @@ -63,14 +64,14 @@ class HsManagedServerHostingAssetValidatorUnitTest { } @Test - void validatesParentAndAssignedToAssetMustNotBeSet() { + void rejectsInvalidReferencedEntities() { // given final var mangedServerHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_SERVER) .identifier("xyz00") - .parentAsset(HsHostingAssetEntity.builder().build()) - .assignedToAsset(HsHostingAssetEntity.builder().build()) .bookingItem(TEST_CLOUD_SERVER_BOOKING_ITEM) + .parentAsset(HsHostingAssetEntity.builder().type(CLOUD_SERVER).build()) + .assignedToAsset(HsHostingAssetEntity.builder().type(MANAGED_SERVER).build()) .build(); final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); @@ -80,7 +81,7 @@ class HsManagedServerHostingAssetValidatorUnitTest { // then assertThat(result).containsExactlyInAnyOrder( "'MANAGED_SERVER:xyz00.bookingItem' must be of type MANAGED_SERVER but is of type CLOUD_SERVER", - "'MANAGED_SERVER:xyz00.parentAsset' must be null but is set to D-1234500:test project:test cloud server booking item", - "'MANAGED_SERVER:xyz00.assignedToAsset' must be null but is set to D-1234500:test project:test cloud server booking item"); + "'MANAGED_SERVER:xyz00.parentAsset' must be null but is of type CLOUD_SERVER", + "'MANAGED_SERVER:xyz00.assignedToAsset' must be null but is of type MANAGED_SERVER"); } } 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 1d2c6d24..7e353147 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 @@ -10,6 +10,7 @@ import java.util.Map; import java.util.stream.Stream; import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_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; @@ -142,7 +143,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { } @Test - void validatesEntityReferences() { + void rejectsInvalidEntityReferences() { // given final var validator = HsHostingAssetEntityValidatorRegistry.forType(MANAGED_WEBSPACE); final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() @@ -153,7 +154,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { .resources(Map.ofEntries(entry("SSD", 25), entry("Traffic", 250))) .build()) .parentAsset(cloudServerAssetEntity) - .assignedToAsset(HsHostingAssetEntity.builder().build()) + .assignedToAsset(HsHostingAssetEntity.builder().type(CLOUD_SERVER).build()) .identifier("abc00") .build(); @@ -163,7 +164,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { // then assertThat(result).containsExactly( "'MANAGED_WEBSPACE:abc00.bookingItem' must be of type MANAGED_WEBSPACE but is of type MANAGED_SERVER", - "'MANAGED_WEBSPACE:abc00.parentAsset' must be of type MANAGED_SERVER but is of type CLOUD_SERVER", - "'MANAGED_WEBSPACE:abc00.assignedToAsset' must be null but is set to D-???????-?:some ManagedServer"); + "'MANAGED_WEBSPACE:abc00.parentAsset' must be null or of type MANAGED_SERVER but is of type CLOUD_SERVER", + "'MANAGED_WEBSPACE:abc00.assignedToAsset' must be null but is of type CLOUD_SERVER"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/system/SystemProcessTest.java b/src/test/java/net/hostsharing/hsadminng/system/SystemProcessUnitTest.java similarity index 98% rename from src/test/java/net/hostsharing/hsadminng/system/SystemProcessTest.java rename to src/test/java/net/hostsharing/hsadminng/system/SystemProcessUnitTest.java index e1924adf..5025555c 100644 --- a/src/test/java/net/hostsharing/hsadminng/system/SystemProcessTest.java +++ b/src/test/java/net/hostsharing/hsadminng/system/SystemProcessUnitTest.java @@ -9,7 +9,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.junit.jupiter.api.condition.OS.LINUX; -class SystemProcessTest { +class SystemProcessUnitTest { @Test @EnabledOnOs(LINUX) From 0af389d7c67fa684e8802c2e2c4a2e7ef606f41f Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Wed, 10 Jul 2024 15:54:02 +0200 Subject: [PATCH 17/18] add-domain-http-setup-validation (#73) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/73 Reviewed-by: Timotheus Pokorra --- .../asset/HsHostingAssetController.java | 10 +- .../asset/HsHostingAssetPropsController.java | 6 +- ...a => HostingAssetEntitySaveProcessor.java} | 18 +- ....java => HostingAssetEntityValidator.java} | 8 +- ... HostingAssetEntityValidatorRegistry.java} | 3 +- .../HsCloudServerHostingAssetValidator.java | 2 +- ...HsDomainDnsSetupHostingAssetValidator.java | 2 +- ...sDomainHttpSetupHostingAssetValidator.java | 56 ++++++ .../HsDomainSetupHostingAssetValidator.java | 6 +- .../HsEMailAliasHostingAssetValidator.java | 2 +- .../HsManagedServerHostingAssetValidator.java | 2 +- ...sManagedWebspaceHostingAssetValidator.java | 2 +- .../HsUnixUserHostingAssetValidator.java | 2 +- .../hs/validation/StringProperty.java | 11 +- .../hs/validation/ValidatableProperty.java | 4 +- .../HsHostingAssetControllerRestTest.java | 53 ++++++ ...ingAssetPropsControllerAcceptanceTest.java | 3 +- ...AssetEntityValidatorRegistryUnitTest.java} | 9 +- ...udServerHostingAssetValidatorUnitTest.java | 10 +- ...DnsSetupHostingAssetValidatorUnitTest.java | 22 +-- ...ttpSetupHostingAssetValidatorUnitTest.java | 163 ++++++++++++++++++ ...ainSetupHostingAssetValidatorUnitTest.java | 8 +- ...ailAliasHostingAssetValidatorUnitTest.java | 10 +- ...edServerHostingAssetValidatorUnitTest.java | 8 +- ...WebspaceHostingAssetValidatorUnitTest.java | 10 +- ...UnixUserHostingAssetValidatorUnitTest.java | 14 +- 26 files changed, 363 insertions(+), 81 deletions(-) rename src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/{HsHostingAssetEntityProcessor.java => HostingAssetEntitySaveProcessor.java} (81%) rename src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/{HsHostingAssetEntityValidator.java => HostingAssetEntityValidator.java} (96%) rename src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/{HsHostingAssetEntityValidatorRegistry.java => HostingAssetEntityValidatorRegistry.java} (94%) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidator.java rename src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/{HsHostingAssetEntityValidatorRegistryUnitTest.java => HostingAssetEntityValidatorRegistryUnitTest.java} (80%) create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java 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 6e082c05..3ca7efff 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 @@ -1,8 +1,8 @@ package net.hostsharing.hsadminng.hs.hosting.asset; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; -import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityProcessor; -import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry; +import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor; +import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityValidatorRegistry; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetsApi; import net.hostsharing.hsadminng.context.Context; @@ -72,7 +72,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { final var entity = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); - final var mapped = new HsHostingAssetEntityProcessor(entity) + final var mapped = new HostingAssetEntitySaveProcessor(entity) .preprocessEntity() .validateEntity() .prepareForSave() @@ -133,7 +133,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { new HsHostingAssetEntityPatcher(em, entity).apply(body); - final var mapped = new HsHostingAssetEntityProcessor(entity) + final var mapped = new HostingAssetEntitySaveProcessor(entity) .preprocessEntity() .validateEntity() .prepareForSave() @@ -161,6 +161,6 @@ public class HsHostingAssetController implements HsHostingAssetsApi { @SuppressWarnings("unchecked") final BiConsumer ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) - -> resource.setConfig(HsHostingAssetEntityValidatorRegistry.forType(entity.getType()) + -> resource.setConfig(HostingAssetEntityValidatorRegistry.forType(entity.getType()) .revampProperties(entity, (Map) resource.getConfig())); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java index 0da530bd..ca8bbb08 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsController.java @@ -1,6 +1,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset; -import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry; +import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityValidatorRegistry; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetPropsApi; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource; import org.springframework.http.ResponseEntity; @@ -15,7 +15,7 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi { @Override public ResponseEntity> listAssetTypes() { - final var resource = HsHostingAssetEntityValidatorRegistry.types().stream() + final var resource = HostingAssetEntityValidatorRegistry.types().stream() .map(Enum::name) .toList(); return ResponseEntity.ok(resource); @@ -26,7 +26,7 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi { final HsHostingAssetTypeResource assetType) { final Enum type = HsHostingAssetType.of(assetType); - final var propValidators = HsHostingAssetEntityValidatorRegistry.forType(type); + final var propValidators = HostingAssetEntityValidatorRegistry.forType(type); final List> resource = propValidators.properties(); return ResponseEntity.ok(toListOfObjects(resource)); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityProcessor.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntitySaveProcessor.java similarity index 81% rename from src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityProcessor.java rename to src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntitySaveProcessor.java index cc192bc7..189b3314 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityProcessor.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntitySaveProcessor.java @@ -11,27 +11,27 @@ import java.util.function.Function; /** * Wraps the steps of the pararation, validation, mapping and revamp around saving of a HsHostingAssetEntity into a readable API. */ -public class HsHostingAssetEntityProcessor { +public class HostingAssetEntitySaveProcessor { private final HsEntityValidator validator; private String expectedStep = "preprocessEntity"; private HsHostingAssetEntity entity; private HsHostingAssetResource resource; - public HsHostingAssetEntityProcessor(final HsHostingAssetEntity entity) { + public HostingAssetEntitySaveProcessor(final HsHostingAssetEntity entity) { this.entity = entity; - this.validator = HsHostingAssetEntityValidatorRegistry.forType(entity.getType()); + this.validator = HostingAssetEntityValidatorRegistry.forType(entity.getType()); } /// initial step allowing to set default values before any validations - public HsHostingAssetEntityProcessor preprocessEntity() { + public HostingAssetEntitySaveProcessor preprocessEntity() { step("preprocessEntity", "validateEntity"); validator.preprocessEntity(entity); return this; } /// validates the entity itself including its properties - public HsHostingAssetEntityProcessor validateEntity() { + public HostingAssetEntitySaveProcessor validateEntity() { step("validateEntity", "prepareForSave"); MultiValidationException.throwIfNotEmpty(validator.validateEntity(entity)); return this; @@ -39,27 +39,27 @@ public class HsHostingAssetEntityProcessor { /// hashing passwords etc. @SuppressWarnings("unchecked") - public HsHostingAssetEntityProcessor prepareForSave() { + public HostingAssetEntitySaveProcessor prepareForSave() { step("prepareForSave", "saveUsing"); validator.prepareProperties(entity); return this; } - public HsHostingAssetEntityProcessor saveUsing(final Function saveFunction) { + public HostingAssetEntitySaveProcessor saveUsing(final Function saveFunction) { step("saveUsing", "validateContext"); entity = saveFunction.apply(entity); return this; } /// validates the entity within it's parent and child hierarchy (e.g. totals validators and other limits) - public HsHostingAssetEntityProcessor validateContext() { + public HostingAssetEntitySaveProcessor validateContext() { step("validateContext", "mapUsing"); MultiValidationException.throwIfNotEmpty(validator.validateContext(entity)); return this; } /// maps entity to JSON resource representation - public HsHostingAssetEntityProcessor mapUsing( + public HostingAssetEntitySaveProcessor mapUsing( final Function mapFunction) { step("mapUsing", "revampProperties"); resource = mapFunction.apply(entity); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidator.java similarity index 96% rename from src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java rename to src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidator.java index 187630fb..0c0282e0 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidator.java @@ -21,16 +21,16 @@ import static java.util.Arrays.stream; import static java.util.Collections.emptyList; import static java.util.Optional.ofNullable; -public abstract class HsHostingAssetEntityValidator extends HsEntityValidator { +public abstract class HostingAssetEntityValidator extends HsEntityValidator { static final ValidatableProperty[] NO_EXTRA_PROPERTIES = new ValidatableProperty[0]; private final ReferenceValidator bookingItemReferenceValidation; private final ReferenceValidator parentAssetReferenceValidation; private final ReferenceValidator assignedToAssetReferenceValidation; - private final HsHostingAssetEntityValidator.AlarmContact alarmContactValidation; + private final HostingAssetEntityValidator.AlarmContact alarmContactValidation; - HsHostingAssetEntityValidator( + HostingAssetEntityValidator( final HsHostingAssetType assetType, final AlarmContact alarmContactValidation, final ValidatableProperty... properties) { @@ -98,7 +98,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator, HsEntityValidator> validators = new HashMap<>(); static { @@ -22,6 +22,7 @@ public class HsHostingAssetEntityValidatorRegistry { register(EMAIL_ALIAS, new HsEMailAliasHostingAssetValidator()); register(DOMAIN_SETUP, new HsDomainSetupHostingAssetValidator()); register(DOMAIN_DNS_SETUP, new HsDomainDnsSetupHostingAssetValidator()); + register(DOMAIN_HTTP_SETUP, new HsDomainHttpSetupHostingAssetValidator()); } private static void register(final Enum type, final HsEntityValidator validator) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidator.java index 9413dcf2..840e5841 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidator.java @@ -6,7 +6,7 @@ import java.util.regex.Pattern; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER; -class HsCloudServerHostingAssetValidator extends HsHostingAssetEntityValidator { +class HsCloudServerHostingAssetValidator extends HostingAssetEntityValidator { HsCloudServerHostingAssetValidator() { super( diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java index c263be60..06e8b72a 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java @@ -15,7 +15,7 @@ import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanPro import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; -class HsDomainDnsSetupHostingAssetValidator extends HsHostingAssetEntityValidator { +class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityValidator { // according to RFC 1035 (section 5) and RFC 1034 static final String RR_REGEX_NAME = "([a-z0-9\\.-]+|@)\\s+"; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidator.java new file mode 100644 index 00000000..9065f7d9 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidator.java @@ -0,0 +1,56 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; + +import java.util.regex.Pattern; + +import static java.util.Optional.ofNullable; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_HTTP_SETUP; +import static net.hostsharing.hsadminng.hs.validation.ArrayProperty.arrayOf; +import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty; +import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; + +class HsDomainHttpSetupHostingAssetValidator extends HostingAssetEntityValidator { + + public static final String IDENTIFIER_SUFFIX = "|HTTP"; + public static final String FILESYSTEM_PATH = "^/"; + public static final String PARTIAL_DOMAIN_NAME_REGEX = "(?!-)[A-Za-z0-9-]{1,63}(? entity.setIdentifier(pa.getIdentifier() + IDENTIFIER_SUFFIX)); + } + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java index e16b1356..483472a7 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java @@ -7,9 +7,9 @@ import java.util.regex.Pattern; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP; -class HsDomainSetupHostingAssetValidator extends HsHostingAssetEntityValidator { +class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { - public static final String DOMAIN_NAME_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?> extends ValidatableProp protected static final String[] KEY_ORDER = Array.join( ValidatableProperty.KEY_ORDER_HEAD, - Array.of("matchesRegEx", "minLength", "maxLength"), + Array.of("matchesRegEx", "minLength", "maxLength", "provided"), ValidatableProperty.KEY_ORDER_TAIL, Array.of("undisclosed")); + private String[] provided; private Pattern[] matchesRegEx; private Integer minLength; private Integer maxLength; @@ -50,6 +51,12 @@ public class StringProperty

> extends ValidatableProp return self(); } + /// predifined values, similar to fixed values in a combobox + public P provided(final String... provided) { + this.provided = provided; + return self(); + } + /** * The property value is not disclosed in error messages. * @@ -70,7 +77,7 @@ public class StringProperty

> extends ValidatableProp } if (matchesRegEx != null && stream(matchesRegEx).map(p -> p.matcher(propValue)).noneMatch(Matcher::matches)) { - result.add(propertyName + "' is expected to match any of " + Arrays.toString(matchesRegEx) + " but " + display(propValue) + " does not match any"); + result.add(propertyName + "' is expected to match any of " + Arrays.toString(matchesRegEx) + " but " + display(propValue) + " does not match" + (matchesRegEx.length>1?" any":"")); } if (isReadOnly() && propValue != null) { result.add(propertyName + "' is readonly but given as " + display(propValue)); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java index 346ee08b..01daf6aa 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java @@ -182,8 +182,8 @@ protected void setDeferredInit(final Function[], T[]> //noinspection unchecked validate(result, (T) propValue, propsProvider); } else { - result.add(propertyName + "' is expected to be of type " + type + ", " + - "but is of type '" + propValue.getClass().getSimpleName() + "'"); + result.add(propertyName + "' is expected to be of type " + type.getSimpleName() + ", " + + "but is of type " + propValue.getClass().getSimpleName() + ""); } } return result; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java index eed85585..bdaacf04 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java @@ -246,6 +246,59 @@ public class HsHostingAssetControllerRestTest { } } ] + """), + DOMAIN_HTTP_SETUP( + List.of( + HsHostingAssetEntity.builder() + .type(HsHostingAssetType.DOMAIN_HTTP_SETUP) + .identifier("example.org") + .caption("some fake Domain-HTTP-Setup") + .config(Map.ofEntries( + entry("htdocsfallback", false), + entry("indexes", false), + entry("cgi", false), + entry("passenger", false), + entry("passenger-errorpage", true), + entry("fastcgi", false), + entry("autoconfig", false), + entry("greylisting", false), + entry("includes", false), + entry("letsencrypt", false), + entry("multiviews", false), + entry("fcgi-php-bin", "/usr/lib/cgi-bin/php8"), + entry("passenger-nodejs", "/usr/bin/node-js7"), + entry("passenger-python", "/usr/bin/python6"), + entry("passenger-ruby", "/usr/bin/ruby5"), + entry("subdomains", Array.of("www", "test1", "test2")) + )) + .build()), + """ + [ + { + "type": "DOMAIN_HTTP_SETUP", + "identifier": "example.org", + "caption": "some fake Domain-HTTP-Setup", + "alarmContact": null, + "config": { + "autoconfig": false, + "cgi": false, + "fastcgi": false, + "greylisting": false, + "htdocsfallback": false, + "includes": false, + "indexes": false, + "letsencrypt": false, + "multiviews": false, + "passenger": false, + "passenger-errorpage": true, + "passenger-nodejs": "/usr/bin/node-js7", + "passenger-python": "/usr/bin/python6", + "passenger-ruby": "/usr/bin/ruby5", + "fcgi-php-bin": "/usr/lib/cgi-bin/php8", + "subdomains": ["www","test1","test2"] + } + } + ] """); final HsHostingAssetType assetType; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java index bd571075..dd4afc09 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java @@ -37,7 +37,8 @@ class HsHostingAssetPropsControllerAcceptanceTest { "UNIX_USER", "EMAIL_ALIAS", "DOMAIN_SETUP", - "DOMAIN_DNS_SETUP" + "DOMAIN_DNS_SETUP", + "DOMAIN_HTTP_SETUP" ] """)); // @formatter:on diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidatorRegistryUnitTest.java similarity index 80% rename from src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java rename to src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidatorRegistryUnitTest.java index 4e4abae9..daf0704f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidatorRegistryUnitTest.java @@ -6,13 +6,13 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; -class HsHostingAssetEntityValidatorRegistryUnitTest { +class HostingAssetEntityValidatorRegistryUnitTest { @Test void forTypeWithUnknownTypeThrowsException() { // when final var thrown = catchThrowable(() -> { - HsHostingAssetEntityValidatorRegistry.forType(null); + HostingAssetEntityValidatorRegistry.forType(null); }); // then @@ -22,7 +22,7 @@ class HsHostingAssetEntityValidatorRegistryUnitTest { @Test void typesReturnsAllImplementedTypes() { // when - final var types = HsHostingAssetEntityValidatorRegistry.types(); + final var types = HostingAssetEntityValidatorRegistry.types(); // then // TODO.test: when all types are implemented, replace with set of all types: @@ -35,7 +35,8 @@ class HsHostingAssetEntityValidatorRegistryUnitTest { HsHostingAssetType.UNIX_USER, HsHostingAssetType.EMAIL_ALIAS, HsHostingAssetType.DOMAIN_SETUP, - HsHostingAssetType.DOMAIN_DNS_SETUP + HsHostingAssetType.DOMAIN_DNS_SETUP, + HsHostingAssetType.DOMAIN_HTTP_SETUP ); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java index 8361c5f4..b7e3516a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java @@ -25,7 +25,7 @@ class HsCloudServerHostingAssetValidatorUnitTest { entry("RAM", 2000) )) .build(); - final var validator = HsHostingAssetEntityValidatorRegistry.forType(cloudServerHostingAssetEntity.getType()); + final var validator = HostingAssetEntityValidatorRegistry.forType(cloudServerHostingAssetEntity.getType()); // when @@ -45,7 +45,7 @@ class HsCloudServerHostingAssetValidatorUnitTest { .identifier("xyz99") .bookingItem(TEST_CLOUD_SERVER_BOOKING_ITEM) .build(); - final var validator = HsHostingAssetEntityValidatorRegistry.forType(cloudServerHostingAssetEntity.getType()); + final var validator = HostingAssetEntityValidatorRegistry.forType(cloudServerHostingAssetEntity.getType()); // when @@ -59,7 +59,7 @@ class HsCloudServerHostingAssetValidatorUnitTest { @Test void containsAllValidations() { // when - final var validator = HsHostingAssetEntityValidatorRegistry.forType(CLOUD_SERVER); + final var validator = HostingAssetEntityValidatorRegistry.forType(CLOUD_SERVER); // then assertThat(validator.properties()).map(Map::toString).isEmpty(); @@ -73,7 +73,7 @@ class HsCloudServerHostingAssetValidatorUnitTest { .identifier("xyz00") .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build()) .build(); - final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); + final var validator = HostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); // when final var result = validator.validateEntity(mangedServerHostingAssetEntity); @@ -93,7 +93,7 @@ class HsCloudServerHostingAssetValidatorUnitTest { .parentAsset(HsHostingAssetEntity.builder().type(MANAGED_SERVER).build()) .assignedToAsset(HsHostingAssetEntity.builder().type(CLOUD_SERVER).build()) .build(); - final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); + final var validator = HostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); // when final var result = validator.validateEntity(mangedServerHostingAssetEntity); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java index 681196ae..715138ec 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java @@ -46,7 +46,7 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { @Test void containsExpectedProperties() { // when - final var validator = HsHostingAssetEntityValidatorRegistry.forType(DOMAIN_DNS_SETUP); + final var validator = HostingAssetEntityValidatorRegistry.forType(DOMAIN_DNS_SETUP); // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( @@ -75,7 +75,7 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { // given final var givenEntity = validEntityBuilder().build(); assertThat(givenEntity.getParentAsset().getIdentifier()).as("preconditon failed").isEqualTo("example.org"); - final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); // when validator.preprocessEntity(givenEntity); @@ -88,7 +88,7 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { void rejectsInvalidIdentifier() { // given final var givenEntity = validEntityBuilder().identifier("example.org").build(); - final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); // when final var result = validator.validateEntity(givenEntity); @@ -103,7 +103,7 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { void acceptsValidIdentifier() { // given final var givenEntity = validEntityBuilder().identifier(validDomainSetupEntity.getIdentifier()+"|DNS").build(); - final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); // when final var result = validator.validateEntity(givenEntity); @@ -120,7 +120,7 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { .parentAsset(null) .assignedToAsset(HsHostingAssetEntity.builder().type(DOMAIN_SETUP).build()) .build(); - final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); + final var validator = HostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); // when final var result = validator.validateEntity(mangedServerHostingAssetEntity); @@ -136,7 +136,7 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { void acceptsValidEntity() { // given final var givenEntity = validEntityBuilder().build(); - final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); // when final var errors = validator.validateEntity(givenEntity); @@ -146,7 +146,7 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { } @Test - void recectsInvalidProperties() { + void rejectsInvalidProperties() { // given final var mangedServerHostingAssetEntity = validEntityBuilder() .config(Map.ofEntries( @@ -156,14 +156,14 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { "www BAD1 Record-Class missing / not enough columns")) )) .build(); - final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); + final var validator = HostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); // when final var result = validator.validateEntity(mangedServerHostingAssetEntity); // then assertThat(result).containsExactlyInAnyOrder( - "'DOMAIN_DNS_SETUP:example.org|DNS.config.TTL' is expected to be of type class java.lang.Integer, but is of type 'String'", + "'DOMAIN_DNS_SETUP:example.org|DNS.config.TTL' is expected to be of type Integer, but is of type String", "'DOMAIN_DNS_SETUP:example.org|DNS.config.user-RR' is expected to match any of [([a-z0-9\\.-]+|@)\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*IN\\s+[A-Z]+\\s+[^;].*(;.*)*, ([a-z0-9\\.-]+|@)\\s+IN\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*[A-Z]+\\s+[^;].*(;.*)*] but '@ 1814400 IN 1814400 BAD1 TTL only allowed once' does not match any", "'DOMAIN_DNS_SETUP:example.org|DNS.config.user-RR' is expected to match any of [([a-z0-9\\.-]+|@)\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*IN\\s+[A-Z]+\\s+[^;].*(;.*)*, ([a-z0-9\\.-]+|@)\\s+IN\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*[A-Z]+\\s+[^;].*(;.*)*] but 'www BAD1 Record-Class missing / not enough columns' does not match any"); } @@ -200,7 +200,7 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { void generatesZonefile() { // given final var givenEntity = validEntityBuilder().build(); - final var validator = (HsDomainDnsSetupHostingAssetValidator) HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + final var validator = (HsDomainDnsSetupHostingAssetValidator) HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); // when final var zonefile = validator.toZonefileString(givenEntity); @@ -231,7 +231,7 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { )) )) .build(); - final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); // when final var errors = validator.validateContext(givenEntity); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java new file mode 100644 index 00000000..c84dd2b1 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java @@ -0,0 +1,163 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity.HsHostingAssetEntityBuilder; +import net.hostsharing.hsadminng.mapper.Array; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_HTTP_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER; +import static org.assertj.core.api.Assertions.assertThat; + +class HsDomainHttpSetupHostingAssetValidatorUnitTest { + + static final HsHostingAssetEntity validDomainSetupEntity = HsHostingAssetEntity.builder() + .type(DOMAIN_SETUP) + .identifier("example.org") + .build(); + + static HsHostingAssetEntityBuilder validEntityBuilder() { + return HsHostingAssetEntity.builder() + .type(DOMAIN_HTTP_SETUP) + .parentAsset(validDomainSetupEntity) + .assignedToAsset(HsHostingAssetEntity.builder().type(UNIX_USER).build()) + .identifier("example.org|HTTP") + .config(Map.ofEntries( + entry("passenger-errorpage", true), + entry("subdomains", Array.of("www", "test") + ) + )); + } + + @Test + void containsExpectedProperties() { + // when + final var validator = HostingAssetEntityValidatorRegistry.forType(DOMAIN_HTTP_SETUP); + + // then + assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( + "{type=boolean, propertyName=htdocsfallback, defaultValue=true}", + "{type=boolean, propertyName=indexes, defaultValue=true}", + "{type=boolean, propertyName=cgi, defaultValue=true}", + "{type=boolean, propertyName=passenger, defaultValue=true}", + "{type=boolean, propertyName=passenger-errorpage}", + "{type=boolean, propertyName=fastcgi, defaultValue=true}", + "{type=boolean, propertyName=autoconfig, defaultValue=true}", + "{type=boolean, propertyName=greylisting, defaultValue=true}", + "{type=boolean, propertyName=includes, defaultValue=true}", + "{type=boolean, propertyName=letsencrypt, defaultValue=true}", + "{type=boolean, propertyName=multiviews, defaultValue=true}", + "{type=string, propertyName=fcgi-php-bin, matchesRegEx=[^/], provided=[/usr/lib/cgi-bin/php], defaultValue=/usr/lib/cgi-bin/php}", + "{type=string, propertyName=passenger-nodejs, matchesRegEx=[^/], provided=[/usr/bin/node], defaultValue=/usr/bin/node}", + "{type=string, propertyName=passenger-python, matchesRegEx=[^/], provided=[/usr/bin/python3], defaultValue=/usr/bin/python3}", + "{type=string, propertyName=passenger-ruby, matchesRegEx=[^/], provided=[/usr/bin/ruby], defaultValue=/usr/bin/ruby}", + "{type=string[], propertyName=subdomains, elementsOf={type=string, propertyName=subdomains, matchesRegEx=[(?!-)[A-Za-z0-9-]{1,63}(? Date: Thu, 11 Jul 2024 10:43:47 +0200 Subject: [PATCH 18/18] add-domain-email-setup-validation (#74) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/74 Reviewed-by: Timotheus Pokorra --- doc/hs-hosting-asset-type-structure.md | 20 +-- .../hs/booking/item/HsBookingItemType.java | 4 +- ...HsManagedWebspaceBookingItemValidator.java | 4 +- .../hs/hosting/asset/HsHostingAssetType.java | 17 +-- .../HostingAssetEntityValidatorRegistry.java | 3 + ...HsDomainDnsSetupHostingAssetValidator.java | 2 +- ...sDomainHttpSetupHostingAssetValidator.java | 2 +- ...sDomainMboxSetupHostingAssetValidator.java | 34 +++++ .../HsDomainSetupHostingAssetValidator.java | 1 - ...sDomainSmtpSetupHostingAssetValidator.java | 34 +++++ .../HsEMailAddressHostingAssetValidator.java | 51 +++++++ .../hs-hosting/hs-hosting-asset-schemas.yaml | 3 +- .../7010-hs-hosting-asset.sql | 10 +- .../7018-hs-hosting-asset-test-data.sql | 49 ++++--- ...gedServerBookingItemValidatorUnitTest.java | 2 +- .../HsHostingAssetControllerRestTest.java | 68 ++++++++- ...ingAssetPropsControllerAcceptanceTest.java | 5 +- .../asset/HsHostingAssetTypeUnitTest.java | 23 ++- ...gAssetEntityValidatorRegistryUnitTest.java | 5 +- ...DnsSetupHostingAssetValidatorUnitTest.java | 12 +- ...ttpSetupHostingAssetValidatorUnitTest.java | 2 +- ...mainMboxHostingAssetValidatorUnitTest.java | 133 ++++++++++++++++++ ...mtpSetupHostingAssetValidatorUnitTest.java | 133 ++++++++++++++++++ ...lAddressHostingAssetValidatorUnitTest.java | 114 +++++++++++++++ 24 files changed, 653 insertions(+), 78 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainMboxSetupHostingAssetValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSmtpSetupHostingAssetValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAddressHostingAssetValidator.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainMboxHostingAssetValidatorUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSmtpSetupHostingAssetValidatorUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAddressHostingAssetValidatorUnitTest.java diff --git a/doc/hs-hosting-asset-type-structure.md b/doc/hs-hosting-asset-type-structure.md index b03b7ced..f2af876b 100644 --- a/doc/hs-hosting-asset-type-structure.md +++ b/doc/hs-hosting-asset-type-structure.md @@ -12,7 +12,7 @@ package Booking #feb28c { entity BI_MANAGED_SERVER entity BI_MANAGED_WEBSPACE entity BI_DOMAIN_DNS_SETUP - entity BI_DOMAIN_EMAIL_SUBMISSION_SETUP + entity BI_DOMAIN_SMTP_SETUP } package Hosting #feb28c{ @@ -20,8 +20,8 @@ package Hosting #feb28c{ entity HA_DOMAIN_SETUP entity HA_DOMAIN_DNS_SETUP entity HA_DOMAIN_HTTP_SETUP - entity HA_DOMAIN_EMAIL_SUBMISSION_SETUP - entity HA_DOMAIN_EMAIL_MAILBOX_SETUP + entity HA_DOMAIN_SMTP_SETUP + entity HA_DOMAIN_MBOX_SETUP entity HA_EMAIL_ADDRESS } @@ -52,12 +52,12 @@ HA_DOMAIN_SETUP o..> HA_DOMAIN_SETUP HA_DOMAIN_DNS_SETUP *==> HA_DOMAIN_SETUP HA_DOMAIN_HTTP_SETUP *==> HA_DOMAIN_SETUP HA_DOMAIN_HTTP_SETUP o..> HA_UNIX_USER -HA_DOMAIN_EMAIL_SUBMISSION_SETUP *==> HA_DOMAIN_SETUP -HA_DOMAIN_EMAIL_SUBMISSION_SETUP o..> HA_MANAGED_WEBSPACE -HA_DOMAIN_EMAIL_MAILBOX_SETUP *==> HA_DOMAIN_SETUP -HA_DOMAIN_EMAIL_MAILBOX_SETUP o..> HA_MANAGED_WEBSPACE +HA_DOMAIN_SMTP_SETUP *==> HA_DOMAIN_SETUP +HA_DOMAIN_SMTP_SETUP o..> HA_MANAGED_WEBSPACE +HA_DOMAIN_MBOX_SETUP *==> HA_DOMAIN_SETUP +HA_DOMAIN_MBOX_SETUP o..> HA_MANAGED_WEBSPACE HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE -HA_EMAIL_ADDRESS *==> HA_DOMAIN_EMAIL_MAILBOX_SETUP +HA_EMAIL_ADDRESS *==> HA_DOMAIN_MBOX_SETUP HA_IP_NUMBER o..> HA_CLOUD_SERVER HA_IP_NUMBER o..> HA_MANAGED_SERVER HA_IP_NUMBER o..> HA_MANAGED_WEBSPACE @@ -82,7 +82,7 @@ package Booking #feb28c { entity BI_MANAGED_SERVER entity BI_MANAGED_WEBSPACE entity BI_DOMAIN_DNS_SETUP - entity BI_DOMAIN_EMAIL_SUBMISSION_SETUP + entity BI_DOMAIN_SMTP_SETUP } package Hosting #feb28c{ @@ -145,7 +145,7 @@ package Booking #feb28c { entity BI_MANAGED_SERVER entity BI_MANAGED_WEBSPACE entity BI_DOMAIN_DNS_SETUP - entity BI_DOMAIN_EMAIL_SUBMISSION_SETUP + entity BI_DOMAIN_SMTP_SETUP } package Hosting #feb28c{ diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemType.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemType.java index 720b3ecc..8b51def8 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemType.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemType.java @@ -8,9 +8,7 @@ public enum HsBookingItemType implements Node { PRIVATE_CLOUD, CLOUD_SERVER(PRIVATE_CLOUD), MANAGED_SERVER(PRIVATE_CLOUD), - MANAGED_WEBSPACE(MANAGED_SERVER), - DOMAIN_DNS_SETUP, // TODO.spec: experimental - DOMAIN_EMAIL_SUBMISSION_SETUP; // TODO.spec: experimental + MANAGED_WEBSPACE(MANAGED_SERVER); private final HsBookingItemType parentItemType; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java index 2bca0042..1f094f36 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidator.java @@ -8,7 +8,7 @@ import java.util.List; import static java.util.Collections.emptyList; import static java.util.Optional.ofNullable; -import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_EMAIL_MAILBOX_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.EMAIL_ADDRESS; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_DATABASE; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_USER; @@ -88,7 +88,7 @@ class HsManagedWebspaceBookingItemValidator extends HsBookingItemEntityValidator return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> { final var unixUserCount = ofNullable(entity.getRelatedHostingAsset()) .map(ha -> ha.getSubHostingAssets().stream() - .filter(bi -> bi.getType() == DOMAIN_EMAIL_MAILBOX_SETUP) + .filter(bi -> bi.getType() == DOMAIN_MBOX_SETUP) .flatMap(domainEMailSetup -> domainEMailSetup.getSubHostingAssets().stream() .filter(subAsset -> subAsset.getType()==EMAIL_ADDRESS)) .count()) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java index 6a0846a9..0054974b 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java @@ -45,6 +45,10 @@ public enum HsHostingAssetType implements Node { inGroup("Webspace"), requiredParent(MANAGED_WEBSPACE)), + EMAIL_ALIAS( // named e.g. xyz00-abc + inGroup("Webspace"), + requiredParent(MANAGED_WEBSPACE)), + DOMAIN_SETUP( // named e.g. example.org inGroup("Domain"), optionalParent(SAME_TYPE) @@ -52,32 +56,29 @@ public enum HsHostingAssetType implements Node { DOMAIN_DNS_SETUP( // named e.g. example.org inGroup("Domain"), - requiredParent(DOMAIN_SETUP)), + requiredParent(DOMAIN_SETUP), + assignedTo(MANAGED_WEBSPACE)), DOMAIN_HTTP_SETUP( // named e.g. example.org inGroup("Domain"), requiredParent(DOMAIN_SETUP), assignedTo(UNIX_USER)), - DOMAIN_EMAIL_SUBMISSION_SETUP( // named e.g. example.org + DOMAIN_SMTP_SETUP( // named e.g. example.org inGroup("Domain"), requiredParent(DOMAIN_SETUP), assignedTo(MANAGED_WEBSPACE)), - DOMAIN_EMAIL_MAILBOX_SETUP( // named e.g. example.org + DOMAIN_MBOX_SETUP( // named e.g. example.org inGroup("Domain"), requiredParent(DOMAIN_SETUP), assignedTo(MANAGED_WEBSPACE)), // TODO.spec: SECURE_MX - EMAIL_ALIAS( // named e.g. xyz00-abc - inGroup("Webspace"), - requiredParent(MANAGED_WEBSPACE)), - EMAIL_ADDRESS( // named e.g. sample@example.org inGroup("Domain"), - requiredParent(DOMAIN_EMAIL_MAILBOX_SETUP)), + requiredParent(DOMAIN_MBOX_SETUP)), PGSQL_INSTANCE( // TODO.spec: identifier to be specified inGroup("PostgreSQL"), diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidatorRegistry.java index 62e47dec..c44bf92a 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidatorRegistry.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidatorRegistry.java @@ -23,6 +23,9 @@ public class HostingAssetEntityValidatorRegistry { register(DOMAIN_SETUP, new HsDomainSetupHostingAssetValidator()); register(DOMAIN_DNS_SETUP, new HsDomainDnsSetupHostingAssetValidator()); register(DOMAIN_HTTP_SETUP, new HsDomainHttpSetupHostingAssetValidator()); + register(DOMAIN_SMTP_SETUP, new HsDomainSmtpSetupHostingAssetValidator()); + register(DOMAIN_MBOX_SETUP, new HsDomainMboxSetupHostingAssetValidator()); + register(EMAIL_ADDRESS, new HsEMailAddressHostingAssetValidator()); } private static void register(final Enum type, final HsEntityValidator validator) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java index 06e8b72a..97c44ce2 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java @@ -60,7 +60,7 @@ class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityValidator @Override protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { - return Pattern.compile("^" + assetEntity.getParentAsset().getIdentifier() + Pattern.quote(IDENTIFIER_SUFFIX) + "$"); + return Pattern.compile("^" + Pattern.quote(assetEntity.getParentAsset().getIdentifier() + IDENTIFIER_SUFFIX) + "$"); } @Override diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidator.java index 9065f7d9..32a2cb30 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidator.java @@ -43,7 +43,7 @@ class HsDomainHttpSetupHostingAssetValidator extends HostingAssetEntityValidator @Override protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { - return Pattern.compile("^" + assetEntity.getParentAsset().getIdentifier() + Pattern.quote(IDENTIFIER_SUFFIX) + "$"); + return Pattern.compile("^" + Pattern.quote(assetEntity.getParentAsset().getIdentifier() + IDENTIFIER_SUFFIX) + "$"); } @Override diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainMboxSetupHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainMboxSetupHostingAssetValidator.java new file mode 100644 index 00000000..0172fda4 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainMboxSetupHostingAssetValidator.java @@ -0,0 +1,34 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; + +import java.util.regex.Pattern; + +import static java.util.Optional.ofNullable; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP; + +class HsDomainMboxSetupHostingAssetValidator extends HostingAssetEntityValidator { + + public static final String IDENTIFIER_SUFFIX = "|MBOX"; + + HsDomainMboxSetupHostingAssetValidator() { + super( + DOMAIN_MBOX_SETUP, + AlarmContact.isOptional(), + + NO_EXTRA_PROPERTIES); + } + + @Override + protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { + return Pattern.compile("^" + Pattern.quote(assetEntity.getParentAsset().getIdentifier() + IDENTIFIER_SUFFIX) + "$"); + } + + @Override + public void preprocessEntity(final HsHostingAssetEntity entity) { + super.preprocessEntity(entity); + if (entity.getIdentifier() == null) { + ofNullable(entity.getParentAsset()).ifPresent(pa -> entity.setIdentifier(pa.getIdentifier() + IDENTIFIER_SUFFIX)); + } + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java index 483472a7..17031c5e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java @@ -43,7 +43,6 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator { // - user has Admin/Agent-role for all its sub-domains and the direct parent-Domain which are set up at at Hostsharing // - domain has DNS zone with TXT record approval // - parent-domain has DNS zone with TXT record approval - // - dom // // TXT-Record check: // new InitialDirContext().getAttributes("dns:_netblocks.google.com", new String[] { "TXT"}).get("TXT").getAll(); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSmtpSetupHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSmtpSetupHostingAssetValidator.java new file mode 100644 index 00000000..e92eba10 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSmtpSetupHostingAssetValidator.java @@ -0,0 +1,34 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; + +import java.util.regex.Pattern; + +import static java.util.Optional.ofNullable; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SMTP_SETUP; + +class HsDomainSmtpSetupHostingAssetValidator extends HostingAssetEntityValidator { + + public static final String IDENTIFIER_SUFFIX = "|SMTP"; + + HsDomainSmtpSetupHostingAssetValidator() { + super( + DOMAIN_SMTP_SETUP, + AlarmContact.isOptional(), + + NO_EXTRA_PROPERTIES); + } + + @Override + protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { + return Pattern.compile("^" + Pattern.quote(assetEntity.getParentAsset().getIdentifier() + IDENTIFIER_SUFFIX) + "$"); + } + + @Override + public void preprocessEntity(final HsHostingAssetEntity entity) { + super.preprocessEntity(entity); + if (entity.getIdentifier() == null) { + ofNullable(entity.getParentAsset()).ifPresent(pa -> entity.setIdentifier(pa.getIdentifier() + IDENTIFIER_SUFFIX)); + } + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAddressHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAddressHostingAssetValidator.java new file mode 100644 index 00000000..d77451e7 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAddressHostingAssetValidator.java @@ -0,0 +1,51 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; + +import java.util.regex.Pattern; + +import static java.util.Optional.ofNullable; +import static net.hostsharing.hsadminng.hs.validation.ArrayProperty.arrayOf; +import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; + +class HsEMailAddressHostingAssetValidator extends HostingAssetEntityValidator { + + private static final String UNIX_USER_REGEX = "^[a-z][a-z0-9]{2}[0-9]{2}(-[a-z0-9]+)?$"; // also accepts legacy pac-names + private static final String EMAIL_ADDRESS_LOCAL_PART_REGEX = "[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+"; // RFC 5322 + private static final String EMAIL_ADDRESS_DOMAIN_PART_REGEX = "[a-zA-Z0-9.-]+"; + private static final String EMAIL_ADDRESS_FULL_REGEX = "^" + EMAIL_ADDRESS_LOCAL_PART_REGEX + "@" + EMAIL_ADDRESS_DOMAIN_PART_REGEX + "$"; + public static final int EMAIL_ADDRESS_MAX_LENGTH = 320; // according to RFC 5321 and RFC 5322 + + HsEMailAddressHostingAssetValidator() { + super( HsHostingAssetType.EMAIL_ADDRESS, + AlarmContact.isOptional(), + + stringProperty("local-part").matchesRegEx("^" + EMAIL_ADDRESS_LOCAL_PART_REGEX + "$").required(), + stringProperty("sub-domain").matchesRegEx("^" + EMAIL_ADDRESS_LOCAL_PART_REGEX + "$").optional(), + arrayOf( + stringProperty("target").maxLength(EMAIL_ADDRESS_MAX_LENGTH).matchesRegEx(UNIX_USER_REGEX, EMAIL_ADDRESS_FULL_REGEX) + ).required().minLength(1)); + } + + @Override + public void preprocessEntity(final HsHostingAssetEntity entity) { + super.preprocessEntity(entity); + super.preprocessEntity(entity); + if (entity.getIdentifier() == null) { + entity.setIdentifier(combineIdentifier(entity)); + } + } + + @Override + protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { + return Pattern.compile("^"+ Pattern.quote(combineIdentifier(assetEntity)) + "$"); + } + + private static String combineIdentifier(final HsHostingAssetEntity emailAddressAssetEntity) { + return emailAddressAssetEntity.getDirectValue("local-part", String.class) + + ofNullable(emailAddressAssetEntity.getDirectValue("sub-domain", String.class)).map(s -> "." + s).orElse("") + + "@" + + emailAddressAssetEntity.getParentAsset().getIdentifier(); + } +} diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml index a9ab7f64..f4a25607 100644 --- a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml @@ -13,7 +13,8 @@ components: - DOMAIN_SETUP - DOMAIN_DNS_SETUP - DOMAIN_HTTP_SETUP - - DOMAIN_EMAIL_SETUP + - DOMAIN_SMTP_SETUP + - DOMAIN_MBOX_SETUP - EMAIL_ALIAS - EMAIL_ADDRESS - PGSQL_USER 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 eb335238..c1b4bbcc 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 @@ -12,7 +12,8 @@ create type HsHostingAssetType as enum ( 'DOMAIN_SETUP', 'DOMAIN_DNS_SETUP', 'DOMAIN_HTTP_SETUP', - 'DOMAIN_EMAIL_SETUP', + 'DOMAIN_SMTP_SETUP', + 'DOMAIN_MBOX_SETUP', 'EMAIL_ALIAS', 'EMAIL_ADDRESS', 'PGSQL_USER', @@ -64,12 +65,13 @@ begin when 'MANAGED_SERVER' then null when 'MANAGED_WEBSPACE' then 'MANAGED_SERVER' when 'UNIX_USER' then 'MANAGED_WEBSPACE' + when 'EMAIL_ALIAS' then 'MANAGED_WEBSPACE' when 'DOMAIN_SETUP' then null when 'DOMAIN_DNS_SETUP' then 'DOMAIN_SETUP' when 'DOMAIN_HTTP_SETUP' then 'DOMAIN_SETUP' - when 'DOMAIN_EMAIL_SETUP' then 'DOMAIN_SETUP' - when 'EMAIL_ALIAS' then 'MANAGED_WEBSPACE' - when 'EMAIL_ADDRESS' then 'DOMAIN_EMAIL_SETUP' + when 'DOMAIN_SMTP_SETUP' then 'DOMAIN_SETUP' + when 'DOMAIN_MBOX_SETUP' then 'DOMAIN_SETUP' + when 'EMAIL_ADDRESS' then 'DOMAIN_MBOX_SETUP' when 'PGSQL_USER' then 'MANAGED_WEBSPACE' when 'PGSQL_DATABASE' then 'MANAGED_WEBSPACE' when 'MARIADB_USER' then 'MANAGED_WEBSPACE' 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 26ef2ac8..f43edef0 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 @@ -14,16 +14,17 @@ declare currentTask varchar; relatedProject hs_booking_project; relatedDebitor hs_office_debitor; - relatedPrivateCloudBookingItem hs_booking_item; - relatedManagedServerBookingItem hs_booking_item; - relatedCloudServerBookingItem hs_booking_item; - relatedManagedWebspaceBookingItem hs_booking_item; + privateCloudBI hs_booking_item; + managedServerBI hs_booking_item; + cloudServerBI hs_booking_item; + managedWebspaceBI hs_booking_item; debitorNumberSuffix varchar; defaultPrefix varchar; managedServerUuid uuid; managedWebspaceUuid uuid; webUnixUserUuid uuid; domainSetupUuid uuid; + domainMBoxSetupUuid uuid; begin currentTask := 'creating hosting-asset test-data ' || givenProjectCaption; call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN'); @@ -39,47 +40,51 @@ begin where debitor.uuid = relatedProject.debitorUuid; assert relatedDebitor.uuid is not null, 'relatedDebitor for "' || givenProjectCaption || '" must not be null'; - select item.* into relatedPrivateCloudBookingItem + select item.* into privateCloudBI from hs_booking_item item where item.projectUuid = relatedProject.uuid and item.type = 'PRIVATE_CLOUD'; - assert relatedPrivateCloudBookingItem.uuid is not null, 'relatedPrivateCloudBookingItem for "' || givenProjectCaption|| '" must not be null'; + assert privateCloudBI.uuid is not null, 'relatedPrivateCloudBookingItem for "' || givenProjectCaption|| '" must not be null'; - select item.* into relatedManagedServerBookingItem + select item.* into managedServerBI from hs_booking_item item where item.projectUuid = relatedProject.uuid and item.type = 'MANAGED_SERVER'; - assert relatedManagedServerBookingItem.uuid is not null, 'relatedManagedServerBookingItem for "' || givenProjectCaption|| '" must not be null'; + assert managedServerBI.uuid is not null, 'relatedManagedServerBookingItem for "' || givenProjectCaption|| '" must not be null'; - select item.* into relatedCloudServerBookingItem + select item.* into cloudServerBI from hs_booking_item item - where item.parentItemuuid = relatedPrivateCloudBookingItem.uuid + where item.parentItemuuid = privateCloudBI.uuid and item.type = 'CLOUD_SERVER'; - assert relatedCloudServerBookingItem.uuid is not null, 'relatedCloudServerBookingItem for "' || givenProjectCaption|| '" must not be null'; + assert cloudServerBI.uuid is not null, 'relatedCloudServerBookingItem for "' || givenProjectCaption|| '" must not be null'; - select item.* into relatedManagedWebspaceBookingItem + select item.* into managedWebspaceBI from hs_booking_item item where item.projectUuid = relatedProject.uuid and item.type = 'MANAGED_WEBSPACE'; - assert relatedManagedWebspaceBookingItem.uuid is not null, 'relatedManagedWebspaceBookingItem for "' || givenProjectCaption|| '" must not be null'; + assert managedWebspaceBI.uuid is not null, 'relatedManagedWebspaceBookingItem for "' || givenProjectCaption|| '" must not be null'; select uuid_generate_v4() into managedServerUuid; select uuid_generate_v4() into managedWebspaceUuid; select uuid_generate_v4() into webUnixUserUuid; select uuid_generate_v4() into domainSetupUuid; + select uuid_generate_v4() into domainMBoxSetupUuid; debitorNumberSuffix := relatedDebitor.debitorNumberSuffix; defaultPrefix := relatedDebitor.defaultPrefix; insert into hs_hosting_asset - (uuid, bookingitemuuid, type, parentAssetUuid, assignedToAssetUuid, identifier, caption, config) - values (managedServerUuid, relatedManagedServerBookingItem.uuid, 'MANAGED_SERVER', null, null, 'vm10' || debitorNumberSuffix, 'some ManagedServer', '{ "monit_max_cpu_usage": 90, "monit_max_ram_usage": 80, "monit_max_ssd_usage": 70 }'::jsonb), - (uuid_generate_v4(), relatedCloudServerBookingItem.uuid, 'CLOUD_SERVER', null, null, 'vm20' || debitorNumberSuffix, 'another CloudServer', '{}'::jsonb), - (managedWebspaceUuid, relatedManagedWebspaceBookingItem.uuid, 'MANAGED_WEBSPACE', managedServerUuid, null, defaultPrefix || '01', 'some Webspace', '{}'::jsonb), - (uuid_generate_v4(), null, 'EMAIL_ALIAS', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some E-Mail-Alias', '{ "target": [ "office@example.org", "archive@example.com" ] }'::jsonb), - (webUnixUserUuid, null, 'UNIX_USER', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some UnixUser for Website', '{ "SSD-soft-quota": "128", "SSD-hard-quota": "256", "HDD-soft-quota": "512", "HDD-hard-quota": "1024"}'::jsonb), - (domainSetupUuid, null, 'DOMAIN_SETUP', null, null, defaultPrefix || '.example.org', 'some Domain-Setup', '{}'::jsonb), - (uuid_generate_v4(), null, 'DOMAIN_DNS_SETUP', domainSetupUuid, null, defaultPrefix || '.example.org|DNS', 'some Domain-DNS-Setup', '{}'::jsonb), - (uuid_generate_v4(), null, 'DOMAIN_HTTP_SETUP', domainSetupUuid, webUnixUserUuid, defaultPrefix || '.example.org|HTTP', 'some Domain-HTTP-Setup', '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*"}'::jsonb); + (uuid, bookingitemuuid, type, parentAssetUuid, assignedToAssetUuid, identifier, caption, config) + values (managedServerUuid, managedServerBI.uuid, 'MANAGED_SERVER', null, null, 'vm10' || debitorNumberSuffix, 'some ManagedServer', '{ "monit_max_cpu_usage": 90, "monit_max_ram_usage": 80, "monit_max_ssd_usage": 70 }'::jsonb), + (uuid_generate_v4(), cloudServerBI.uuid, 'CLOUD_SERVER', null, null, 'vm20' || debitorNumberSuffix, 'another CloudServer', '{}'::jsonb), + (managedWebspaceUuid, managedWebspaceBI.uuid, 'MANAGED_WEBSPACE', managedServerUuid, null, defaultPrefix || '01', 'some Webspace', '{}'::jsonb), + (uuid_generate_v4(), null, 'EMAIL_ALIAS', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some E-Mail-Alias', '{ "target": [ "office@example.org", "archive@example.com" ] }'::jsonb), + (webUnixUserUuid, null, 'UNIX_USER', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some UnixUser for Website', '{ "SSD-soft-quota": "128", "SSD-hard-quota": "256", "HDD-soft-quota": "512", "HDD-hard-quota": "1024"}'::jsonb), + (domainSetupUuid, null, 'DOMAIN_SETUP', null, null, defaultPrefix || '.example.org', 'some Domain-Setup', '{}'::jsonb), + (uuid_generate_v4(), null, 'DOMAIN_DNS_SETUP', domainSetupUuid, null, defaultPrefix || '.example.org|DNS', 'some Domain-DNS-Setup', '{}'::jsonb), + (uuid_generate_v4(), null, 'DOMAIN_HTTP_SETUP', domainSetupUuid, webUnixUserUuid, defaultPrefix || '.example.org|HTTP', 'some Domain-HTTP-Setup', '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*"}'::jsonb), + (uuid_generate_v4(), null, 'DOMAIN_SMTP_SETUP', domainSetupUuid, managedWebspaceUuid, defaultPrefix || '.example.org|DNS', 'some Domain-SMPT-Setup', '{}'::jsonb), + (domainMBoxSetupUuid, null, 'DOMAIN_MBOX_SETUP', domainSetupUuid, managedWebspaceUuid, defaultPrefix || '.example.org|DNS', 'some Domain-MBOX-Setup', '{}'::jsonb), + (uuid_generate_v4(), null, 'EMAIL_ADDRESS', domainMBoxSetupUuid, null, 'test@' || defaultPrefix || '.example.org', 'some E-Mail-Address', '{}'::jsonb); end; $$; --// diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java index b0605239..5f95e598 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java @@ -152,7 +152,7 @@ class HsManagedServerBookingItemValidatorUnitTest { "xyz00_%c%c", 2, HsHostingAssetType.MARIADB_DATABASE ), - generateDomainEmailSetupsWithEMailAddresses(26, HsHostingAssetType.DOMAIN_EMAIL_MAILBOX_SETUP, + generateDomainEmailSetupsWithEMailAddresses(26, HsHostingAssetType.DOMAIN_MBOX_SETUP, "%c%c.example.com", 10, HsHostingAssetType.EMAIL_ADDRESS ) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java index bdaacf04..4a5bc04c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java @@ -251,7 +251,7 @@ public class HsHostingAssetControllerRestTest { List.of( HsHostingAssetEntity.builder() .type(HsHostingAssetType.DOMAIN_HTTP_SETUP) - .identifier("example.org") + .identifier("example.org|HTTP") .caption("some fake Domain-HTTP-Setup") .config(Map.ofEntries( entry("htdocsfallback", false), @@ -276,7 +276,7 @@ public class HsHostingAssetControllerRestTest { [ { "type": "DOMAIN_HTTP_SETUP", - "identifier": "example.org", + "identifier": "example.org|HTTP", "caption": "some fake Domain-HTTP-Setup", "alarmContact": null, "config": { @@ -299,6 +299,70 @@ public class HsHostingAssetControllerRestTest { } } ] + """), + DOMAIN_SMTP_SETUP( + List.of( + HsHostingAssetEntity.builder() + .type(HsHostingAssetType.DOMAIN_SMTP_SETUP) + .identifier("example.org|SMTP") + .caption("some fake Domain-SMTP-Setup") + .build()), + """ + [ + { + "type": "DOMAIN_SMTP_SETUP", + "identifier": "example.org|SMTP", + "caption": "some fake Domain-SMTP-Setup", + "alarmContact": null, + "config": {} + } + ] + """), + DOMAIN_MBOX_SETUP( + List.of( + HsHostingAssetEntity.builder() + .type(HsHostingAssetType.DOMAIN_MBOX_SETUP) + .identifier("example.org|MBOX") + .caption("some fake Domain-MBOX-Setup") + .build()), + """ + [ + { + "type": "DOMAIN_MBOX_SETUP", + "identifier": "example.org|MBOX", + "caption": "some fake Domain-MBOX-Setup", + "alarmContact": null, + "config": {} + } + ] + """), + EMAIL_ADDRESS( + List.of( + HsHostingAssetEntity.builder() + .type(HsHostingAssetType.EMAIL_ADDRESS) + .parentAsset(HsHostingAssetEntity.builder() + .type(HsHostingAssetType.DOMAIN_MBOX_SETUP) + .identifier("example.org|MBOX") + .caption("some fake Domain-MBOX-Setup") + .build()) + .identifier("office@example.org") + .caption("some fake EMail-Address") + .config(Map.ofEntries( + entry("target", Array.of("xyz00", "xyz00-abc", "office@example.com")) + )) + .build()), + """ + [ + { + "type": "EMAIL_ADDRESS", + "identifier": "office@example.org", + "caption": "some fake EMail-Address", + "alarmContact": null, + "config": { + "target": ["xyz00","xyz00-abc","office@example.com"] + } + } + ] """); final HsHostingAssetType assetType; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java index dd4afc09..290777ea 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java @@ -38,7 +38,10 @@ class HsHostingAssetPropsControllerAcceptanceTest { "EMAIL_ALIAS", "DOMAIN_SETUP", "DOMAIN_DNS_SETUP", - "DOMAIN_HTTP_SETUP" + "DOMAIN_HTTP_SETUP", + "DOMAIN_SMTP_SETUP", + "DOMAIN_MBOX_SETUP", + "EMAIL_ADDRESS" ] """)); // @formatter:on diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetTypeUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetTypeUnitTest.java index 794c3f25..870e2f8f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetTypeUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetTypeUnitTest.java @@ -25,8 +25,6 @@ class HsHostingAssetTypeUnitTest { entity BI_CLOUD_SERVER entity BI_MANAGED_SERVER entity BI_MANAGED_WEBSPACE - entity BI_DOMAIN_DNS_SETUP - entity BI_DOMAIN_EMAIL_SUBMISSION_SETUP } package Hosting #feb28c{ @@ -34,8 +32,8 @@ class HsHostingAssetTypeUnitTest { entity HA_DOMAIN_SETUP entity HA_DOMAIN_DNS_SETUP entity HA_DOMAIN_HTTP_SETUP - entity HA_DOMAIN_EMAIL_SUBMISSION_SETUP - entity HA_DOMAIN_EMAIL_MAILBOX_SETUP + entity HA_DOMAIN_SMTP_SETUP + entity HA_DOMAIN_MBOX_SETUP entity HA_EMAIL_ADDRESS } @@ -62,16 +60,17 @@ class HsHostingAssetTypeUnitTest { HA_MANAGED_WEBSPACE *==> BI_MANAGED_WEBSPACE HA_MANAGED_WEBSPACE o..> HA_MANAGED_SERVER HA_UNIX_USER *==> HA_MANAGED_WEBSPACE + HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE HA_DOMAIN_SETUP o..> HA_DOMAIN_SETUP HA_DOMAIN_DNS_SETUP *==> HA_DOMAIN_SETUP + HA_DOMAIN_DNS_SETUP o..> HA_MANAGED_WEBSPACE HA_DOMAIN_HTTP_SETUP *==> HA_DOMAIN_SETUP HA_DOMAIN_HTTP_SETUP o..> HA_UNIX_USER - HA_DOMAIN_EMAIL_SUBMISSION_SETUP *==> HA_DOMAIN_SETUP - HA_DOMAIN_EMAIL_SUBMISSION_SETUP o..> HA_MANAGED_WEBSPACE - HA_DOMAIN_EMAIL_MAILBOX_SETUP *==> HA_DOMAIN_SETUP - HA_DOMAIN_EMAIL_MAILBOX_SETUP o..> HA_MANAGED_WEBSPACE - HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE - HA_EMAIL_ADDRESS *==> HA_DOMAIN_EMAIL_MAILBOX_SETUP + HA_DOMAIN_SMTP_SETUP *==> HA_DOMAIN_SETUP + HA_DOMAIN_SMTP_SETUP o..> HA_MANAGED_WEBSPACE + HA_DOMAIN_MBOX_SETUP *==> HA_DOMAIN_SETUP + HA_DOMAIN_MBOX_SETUP o..> HA_MANAGED_WEBSPACE + HA_EMAIL_ADDRESS *==> HA_DOMAIN_MBOX_SETUP HA_IP_NUMBER o..> HA_CLOUD_SERVER HA_IP_NUMBER o..> HA_MANAGED_SERVER HA_IP_NUMBER o..> HA_MANAGED_WEBSPACE @@ -96,8 +95,6 @@ class HsHostingAssetTypeUnitTest { entity BI_CLOUD_SERVER entity BI_MANAGED_SERVER entity BI_MANAGED_WEBSPACE - entity BI_DOMAIN_DNS_SETUP - entity BI_DOMAIN_EMAIL_SUBMISSION_SETUP } package Hosting #feb28c{ @@ -160,8 +157,6 @@ class HsHostingAssetTypeUnitTest { entity BI_CLOUD_SERVER entity BI_MANAGED_SERVER entity BI_MANAGED_WEBSPACE - entity BI_DOMAIN_DNS_SETUP - entity BI_DOMAIN_EMAIL_SUBMISSION_SETUP } package Hosting #feb28c{ diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidatorRegistryUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidatorRegistryUnitTest.java index daf0704f..c1c8a53c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidatorRegistryUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidatorRegistryUnitTest.java @@ -36,7 +36,10 @@ class HostingAssetEntityValidatorRegistryUnitTest { HsHostingAssetType.EMAIL_ALIAS, HsHostingAssetType.DOMAIN_SETUP, HsHostingAssetType.DOMAIN_DNS_SETUP, - HsHostingAssetType.DOMAIN_HTTP_SETUP + HsHostingAssetType.DOMAIN_HTTP_SETUP, + HsHostingAssetType.DOMAIN_SMTP_SETUP, + HsHostingAssetType.DOMAIN_MBOX_SETUP, + HsHostingAssetType.EMAIL_ADDRESS ); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java index 715138ec..7f66379c 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java @@ -12,6 +12,7 @@ import java.util.Map; import static java.util.Map.entry; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_DNS_SETUP; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.TestHsHostingAssetEntities.TEST_MANAGED_WEBSPACE_HOSTING_ASSET; import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsDomainDnsSetupHostingAssetValidator.RR_COMMENT; import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsDomainDnsSetupHostingAssetValidator.RR_RECORD_DATA; import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsDomainDnsSetupHostingAssetValidator.RR_RECORD_TYPE; @@ -23,14 +24,15 @@ import static org.assertj.core.api.Assertions.assertThat; class HsDomainDnsSetupHostingAssetValidatorUnitTest { static final HsHostingAssetEntity validDomainSetupEntity = HsHostingAssetEntity.builder() - .type(DOMAIN_SETUP) - .identifier("example.org") - .build(); + .type(DOMAIN_SETUP) + .identifier("example.org") + .build(); static HsHostingAssetEntityBuilder validEntityBuilder() { return HsHostingAssetEntity.builder() .type(DOMAIN_DNS_SETUP) .parentAsset(validDomainSetupEntity) + .assignedToAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) .identifier("example.org|DNS") .config(Map.ofEntries( entry("user-RR", Array.of( @@ -95,7 +97,7 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { // then assertThat(result).containsExactly( - "'identifier' expected to match '^example.org\\Q|DNS\\E$', but is 'example.org'" + "'identifier' expected to match '^\\Qexample.org|DNS\\E$', but is 'example.org'" ); } @@ -129,7 +131,7 @@ class HsDomainDnsSetupHostingAssetValidatorUnitTest { assertThat(result).containsExactlyInAnyOrder( "'DOMAIN_DNS_SETUP:example.org|DNS.bookingItem' must be null but is of type CLOUD_SERVER", "'DOMAIN_DNS_SETUP:example.org|DNS.parentAsset' must be of type DOMAIN_SETUP but is null", - "'DOMAIN_DNS_SETUP:example.org|DNS.assignedToAsset' must be null but is of type DOMAIN_SETUP"); + "'DOMAIN_DNS_SETUP:example.org|DNS.assignedToAsset' must be of type MANAGED_WEBSPACE but is of type DOMAIN_SETUP"); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java index c84dd2b1..29b4c05b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainHttpSetupHostingAssetValidatorUnitTest.java @@ -87,7 +87,7 @@ class HsDomainHttpSetupHostingAssetValidatorUnitTest { // then assertThat(result).containsExactly( - "'identifier' expected to match '^example.org\\Q|HTTP\\E$', but is 'example.org'" + "'identifier' expected to match '^\\Qexample.org|HTTP\\E$', but is 'example.org'" ); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainMboxHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainMboxHostingAssetValidatorUnitTest.java new file mode 100644 index 00000000..2c08d16f --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainMboxHostingAssetValidatorUnitTest.java @@ -0,0 +1,133 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity.HsHostingAssetEntityBuilder; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; +import static org.assertj.core.api.Assertions.assertThat; + +class HsDomainMboxHostingAssetValidatorUnitTest { + + static final HsHostingAssetEntity validDomainSetupEntity = HsHostingAssetEntity.builder() + .type(DOMAIN_SETUP) + .identifier("example.org") + .build(); + + static HsHostingAssetEntityBuilder validEntityBuilder() { + return HsHostingAssetEntity.builder() + .type(DOMAIN_MBOX_SETUP) + .parentAsset(validDomainSetupEntity) + .assignedToAsset(HsHostingAssetEntity.builder().type(MANAGED_WEBSPACE).build()) + .identifier("example.org|MBOX"); + } + + @Test + void containsExpectedProperties() { + // when + final var validator = HostingAssetEntityValidatorRegistry.forType(DOMAIN_MBOX_SETUP); + + // then + assertThat(validator.properties()).map(Map::toString).isEmpty(); + } + + @Test + void preprocessesTakesIdentifierFromParent() { + // given + final var givenEntity = validEntityBuilder().build(); + assertThat(givenEntity.getParentAsset().getIdentifier()).as("precondition failed").isEqualTo("example.org"); + final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + validator.preprocessEntity(givenEntity); + + // then + assertThat(givenEntity.getIdentifier()).isEqualTo("example.org|MBOX"); + } + + @Test + void rejectsInvalidIdentifier() { + // given + final var givenEntity = validEntityBuilder().identifier("example.org").build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + final var result = validator.validateEntity(givenEntity); + + // then + assertThat(result).containsExactly( + "'identifier' expected to match '^\\Qexample.org|MBOX\\E$', but is 'example.org'" + ); + } + + @Test + void acceptsValidIdentifier() { + // given + final var givenEntity = validEntityBuilder().identifier(validDomainSetupEntity.getIdentifier()+"|MBOX").build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + final var result = validator.validateEntity(givenEntity); + + // then + assertThat(result).isEmpty(); + } + + @Test + void rejectsInvalidReferencedEntities() { + // given + final var mangedServerHostingAssetEntity = validEntityBuilder() + .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build()) + .parentAsset(HsHostingAssetEntity.builder().type(MANAGED_WEBSPACE).build()) + .assignedToAsset(null) + .build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(mangedServerHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'DOMAIN_MBOX_SETUP:example.org|MBOX.bookingItem' must be null but is of type CLOUD_SERVER", + "'DOMAIN_MBOX_SETUP:example.org|MBOX.parentAsset' must be of type DOMAIN_SETUP but is of type MANAGED_WEBSPACE", + "'DOMAIN_MBOX_SETUP:example.org|MBOX.assignedToAsset' must be of type MANAGED_WEBSPACE but is null"); + } + + @Test + void acceptsValidEntity() { + // given + final var givenEntity = validEntityBuilder().build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + final var errors = validator.validateEntity(givenEntity); + + // then + assertThat(errors).isEmpty(); + } + + @Test + void rejectsInvalidProperties() { + // given + final var mangedServerHostingAssetEntity = validEntityBuilder() + .config(Map.ofEntries( + entry("any", "false") + )) + .build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(mangedServerHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'DOMAIN_MBOX_SETUP:example.org|MBOX.config.any' is not expected but is set to 'false'"); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSmtpSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSmtpSetupHostingAssetValidatorUnitTest.java new file mode 100644 index 00000000..014fb9ef --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSmtpSetupHostingAssetValidatorUnitTest.java @@ -0,0 +1,133 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; +import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity.HsHostingAssetEntityBuilder; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SMTP_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; +import static org.assertj.core.api.Assertions.assertThat; + +class HsDomainSmtpSetupHostingAssetValidatorUnitTest { + + static final HsHostingAssetEntity validDomainSetupEntity = HsHostingAssetEntity.builder() + .type(DOMAIN_SETUP) + .identifier("example.org") + .build(); + + static HsHostingAssetEntityBuilder validEntityBuilder() { + return HsHostingAssetEntity.builder() + .type(DOMAIN_SMTP_SETUP) + .parentAsset(validDomainSetupEntity) + .assignedToAsset(HsHostingAssetEntity.builder().type(MANAGED_WEBSPACE).build()) + .identifier("example.org|SMTP"); + } + + @Test + void containsExpectedProperties() { + // when + final var validator = HostingAssetEntityValidatorRegistry.forType(DOMAIN_SMTP_SETUP); + + // then + assertThat(validator.properties()).map(Map::toString).isEmpty(); + } + + @Test + void preprocessesTakesIdentifierFromParent() { + // given + final var givenEntity = validEntityBuilder().build(); + assertThat(givenEntity.getParentAsset().getIdentifier()).as("precondition failed").isEqualTo("example.org"); + final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + validator.preprocessEntity(givenEntity); + + // then + assertThat(givenEntity.getIdentifier()).isEqualTo("example.org|SMTP"); + } + + @Test + void rejectsInvalidIdentifier() { + // given + final var givenEntity = validEntityBuilder().identifier("example.org").build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + final var result = validator.validateEntity(givenEntity); + + // then + assertThat(result).containsExactly( + "'identifier' expected to match '^\\Qexample.org|SMTP\\E$', but is 'example.org'" + ); + } + + @Test + void acceptsValidIdentifier() { + // given + final var givenEntity = validEntityBuilder().identifier(validDomainSetupEntity.getIdentifier()+"|SMTP").build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + final var result = validator.validateEntity(givenEntity); + + // then + assertThat(result).isEmpty(); + } + + @Test + void rejectsInvalidReferencedEntities() { + // given + final var mangedServerHostingAssetEntity = validEntityBuilder() + .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build()) + .parentAsset(HsHostingAssetEntity.builder().type(MANAGED_WEBSPACE).build()) + .assignedToAsset(null) + .build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(mangedServerHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'DOMAIN_SMTP_SETUP:example.org|SMTP.bookingItem' must be null but is of type CLOUD_SERVER", + "'DOMAIN_SMTP_SETUP:example.org|SMTP.parentAsset' must be of type DOMAIN_SETUP but is of type MANAGED_WEBSPACE", + "'DOMAIN_SMTP_SETUP:example.org|SMTP.assignedToAsset' must be of type MANAGED_WEBSPACE but is null"); + } + + @Test + void acceptsValidEntity() { + // given + final var givenEntity = validEntityBuilder().build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + final var errors = validator.validateEntity(givenEntity); + + // then + assertThat(errors).isEmpty(); + } + + @Test + void rejectsInvalidProperties() { + // given + final var mangedServerHostingAssetEntity = validEntityBuilder() + .config(Map.ofEntries( + entry("any", "false") + )) + .build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(mangedServerHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'DOMAIN_SMTP_SETUP:example.org|SMTP.config.any' is not expected but is set to 'false'"); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAddressHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAddressHostingAssetValidatorUnitTest.java new file mode 100644 index 00000000..f606f209 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsEMailAddressHostingAssetValidatorUnitTest.java @@ -0,0 +1,114 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.mapper.Array; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static java.util.Map.entry; +import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_SERVER_BOOKING_ITEM; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.EMAIL_ADDRESS; +import static net.hostsharing.hsadminng.hs.hosting.asset.TestHsHostingAssetEntities.TEST_MANAGED_SERVER_HOSTING_ASSET; +import static org.assertj.core.api.Assertions.assertThat; + +class HsEMailAddressHostingAssetValidatorUnitTest { + + final static HsHostingAssetEntity domainMboxetup = HsHostingAssetEntity.builder() + .type(DOMAIN_MBOX_SETUP) + .identifier("example.org") + .build(); + static HsHostingAssetEntity.HsHostingAssetEntityBuilder validEntityBuilder() { + return HsHostingAssetEntity.builder() + .type(EMAIL_ADDRESS) + .parentAsset(domainMboxetup) + .identifier("test@example.org") + .config(Map.ofEntries( + entry("local-part", "test"), + entry("target", Array.of("xyz00", "xyz00-abc", "office@example.com")) + )); + } + + @Test + void containsAllValidations() { + // when + final var validator = HostingAssetEntityValidatorRegistry.forType(EMAIL_ADDRESS); + + // then + assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( + "{type=string, propertyName=local-part, matchesRegEx=[^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+$], required=true}", + "{type=string, propertyName=sub-domain, matchesRegEx=[^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+$]}", + "{type=string[], propertyName=target, elementsOf={type=string, propertyName=target, matchesRegEx=[^[a-z][a-z0-9]{2}[0-9]{2}(-[a-z0-9]+)?$, ^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$], maxLength=320}, required=true, minLength=1}"); + } + + @Test + void acceptsValidEntity() { + // given + final var emailAddressHostingAssetEntity = validEntityBuilder().build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(emailAddressHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(emailAddressHostingAssetEntity); + + // then + assertThat(result).isEmpty(); + } + + @Test + void rejectsInvalidProperties() { + // given + final var emailAddressHostingAssetEntity = validEntityBuilder() + .config(Map.ofEntries( + entry("local-part", "no@allowed"), + entry("sub-domain", "no@allowedeither"), + entry("target", Array.of("xyz00", "xyz00-abc", "garbage", "office@example.com")))) + .build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(emailAddressHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(emailAddressHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'EMAIL_ADDRESS:test@example.org.config.local-part' is expected to match any of [^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+$] but 'no@allowed' does not match", + "'EMAIL_ADDRESS:test@example.org.config.sub-domain' is expected to match any of [^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+$] but 'no@allowedeither' does not match", + "'EMAIL_ADDRESS:test@example.org.config.target' is expected to match any of [^[a-z][a-z0-9]{2}[0-9]{2}(-[a-z0-9]+)?$, ^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$] but 'garbage' does not match any"); + } + + @Test + void rejectsInvalidIdentifier() { + // given + final var emailAddressHostingAssetEntity = validEntityBuilder() + .identifier("abc00-office") + .build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(emailAddressHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(emailAddressHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'identifier' expected to match '^\\Qtest@example.org\\E$', but is 'abc00-office'"); + } + + @Test + void validatesInvalidReferences() { + // given + final var emailAddressHostingAssetEntity = validEntityBuilder() + .bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM) + .parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET) + .assignedToAsset(TEST_MANAGED_SERVER_HOSTING_ASSET) + .build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(emailAddressHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(emailAddressHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'EMAIL_ADDRESS:test@example.org.bookingItem' must be null but is of type MANAGED_SERVER", + "'EMAIL_ADDRESS:test@example.org.parentAsset' must be of type DOMAIN_MBOX_SETUP but is of type MANAGED_SERVER", + "'EMAIL_ADDRESS:test@example.org.assignedToAsset' must be null but is of type MANAGED_SERVER"); + } +}