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..b1da2f06 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,12 +37,10 @@ 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.Permission.DELETE; @@ -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) @@ -84,8 +80,8 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab private int version; @ManyToOne(optional = false) - @JoinColumn(name = "debitoruuid") - private HsOfficeDebitorEntity debitor; + @JoinColumn(name = "projectuuid") + private HsBookingProjectEntity project; @Column(name = "type") @Enumerated(EnumType.STRING) @@ -139,7 +135,7 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab @Override public String toShortString() { - return ofNullable(debitor).map(HsOfficeDebitorEntity::toShortString).orElse("D-???????") + + return ofNullable(project).map(HsBookingProjectEntity::toShortString).orElse("D-???????-?") + ":" + caption; } @@ -156,47 +152,37 @@ 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 + SELECT bookingItem.uuid as uuid, projectIV.idName || '-' || cleanIdentifier(bookingItem.caption) as idName FROM hs_booking_item bookingItem - JOIN hs_office_debitor_iv debitorIV ON debitorIV.uuid = bookingItem.debitorUuid + JOIN hs_booking_project_iv projectIV ON projectIV.uuid = bookingItem.projectUuid """)) .withRestrictedViewOrderBy(SQL.expression("validity")) .withUpdatableColumns("version", "caption", "validity", "resources") - .importEntityAlias("debitor", HsOfficeDebitorEntity.class, usingDefaultCase(), - dependsOnColumn("debitorUuid"), + .importEntityAlias("project", HsBookingProjectEntity.class, usingDefaultCase(), + dependsOnColumn("projectUuid"), 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("project", ADMIN).grantPermission(INSERT) .toRole("global", ADMIN).grantPermission(DELETE) .createRole(OWNER, (with) -> { - with.incomingSuperRole("debitorRel", AGENT); + with.incomingSuperRole("project", AGENT); }) .createSubRole(ADMIN, (with) -> { - with.incomingSuperRole("debitorRel", AGENT); + with.incomingSuperRole("project", AGENT); with.permission(UPDATE); }) .createSubRole(AGENT) .createSubRole(TENANT, (with) -> { - with.outgoingSubRole("debitorRel", TENANT); + with.outgoingSubRole("project", 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..75fa9209 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntity.java @@ -0,0 +1,114 @@ +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("bookingProject", 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.incomingSuperRole("debitorRel", AGENT); + with.permission(UPDATE); + }) + .createSubRole(AGENT) + .createSubRole(TENANT, (with) -> { + with.outgoingSubRole("debitorRel", TENANT); + with.permission(SELECT); + }) + + .limitDiagramTo("bookingProject", "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/HsHostingAssetEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java index 1f4ec01a..8cd628e6 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 static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef.inOtherCas 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; @@ -136,11 +137,7 @@ 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") @@ -177,6 +174,8 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validata with.permission(SELECT); }) + .toRole(GLOBAL, ADMIN).grantPermission(INSERT) + .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/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..d082cc80 --- /dev/null +++ b/src/main/resources/db/changelog/6-hs-booking/610-booking-project/6103-hs-booking-project-rbac.md @@ -0,0 +1,64 @@ +### rbac bookingProject + +This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually. + +```mermaid +%%{init:{'flowchart':{'htmlLabels':false}}}%% +flowchart TB + +subgraph bookingProject["`**bookingProject**`"] + direction TB + style bookingProject fill:#dd4901,stroke:#274d6e,stroke-width:8px + + subgraph bookingProject:roles[ ] + style bookingProject:roles fill:#dd4901,stroke:white + + role:bookingProject:OWNER[[bookingProject:OWNER]] + role:bookingProject:ADMIN[[bookingProject:ADMIN]] + role:bookingProject:AGENT[[bookingProject:AGENT]] + role:bookingProject:TENANT[[bookingProject:TENANT]] + end + + subgraph bookingProject:permissions[ ] + style bookingProject:permissions fill:#dd4901,stroke:white + + perm:bookingProject:INSERT{{bookingProject:INSERT}} + perm:bookingProject:DELETE{{bookingProject:DELETE}} + perm:bookingProject:UPDATE{{bookingProject:UPDATE}} + perm:bookingProject:SELECT{{bookingProject:SELECT}} + end +end + +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 + +%% 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:bookingProject:OWNER +role:bookingProject:OWNER ==> role:bookingProject:ADMIN +role:debitorRel:AGENT ==> role:bookingProject:ADMIN +role:bookingProject:ADMIN ==> role:bookingProject:AGENT +role:bookingProject:AGENT ==> role:bookingProject:TENANT +role:bookingProject:TENANT ==> role:debitorRel:TENANT + +%% granting permissions to roles +role:debitorRel:ADMIN ==> perm:bookingProject:INSERT +role:global:ADMIN ==> perm:bookingProject:DELETE +role:bookingProject:ADMIN ==> perm:bookingProject:UPDATE +role:bookingProject:TENANT ==> perm:bookingProject:SELECT + +``` 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/610-booking-project/6103-hs-booking-project-rbac.sql new file mode 100644 index 00000000..7f4e173e --- /dev/null +++ b/src/main/resources/db/changelog/6-hs-booking/610-booking-project/6103-hs-booking-project-rbac.sql @@ -0,0 +1,208 @@ +--liquibase formatted sql +-- This code generated was by RbacViewPostgresGenerator, do not amend manually. + + +-- ============================================================================ +--changeset hs-booking-project-rbac-OBJECT:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- +call generateRelatedRbacObject('hs_booking_project'); +--// + + +-- ============================================================================ +--changeset hs-booking-project-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- +call generateRbacRoleDescriptors('hsBookingProject', 'hs_booking_project'); +--// + + +-- ============================================================================ +--changeset hs-booking-project-rbac-insert-trigger:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/* + Creates the roles, grants and permission for the AFTER INSERT TRIGGER. + */ + +create or replace procedure buildRbacSystemForHsBookingProject( + NEW hs_booking_project +) + language plpgsql as $$ + +declare + newDebitor hs_office_debitor; + newDebitorRel hs_office_relation; + +begin + call enterTriggerForObjectUuid(NEW.uuid); + + SELECT * FROM hs_office_debitor WHERE uuid = NEW.debitorUuid INTO newDebitor; + assert newDebitor.uuid is not null, format('newDebitor must not be null for NEW.debitorUuid = %s', NEW.debitorUuid); + + SELECT debitorRel.* + FROM hs_office_relation debitorRel + JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid + WHERE debitor.uuid = NEW.debitorUuid + INTO newDebitorRel; + assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorUuid = %s', NEW.debitorUuid); + + + perform createRoleWithGrants( + hsBookingProjectOWNER(NEW), + incomingSuperRoles => array[hsOfficeRelationAGENT(newDebitorRel)] + ); + + perform createRoleWithGrants( + hsBookingProjectADMIN(NEW), + permissions => array['UPDATE'], + incomingSuperRoles => array[ + hsBookingProjectOWNER(NEW), + hsOfficeRelationAGENT(newDebitorRel)] + ); + + perform createRoleWithGrants( + hsBookingProjectAGENT(NEW), + incomingSuperRoles => array[hsBookingProjectADMIN(NEW)] + ); + + perform createRoleWithGrants( + hsBookingProjectTENANT(NEW), + permissions => array['SELECT'], + incomingSuperRoles => array[hsBookingProjectAGENT(NEW)], + outgoingSubRoles => array[hsOfficeRelationTENANT(newDebitorRel)] + ); + + 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_project row. + */ + +create or replace function insertTriggerForHsBookingProject_tf() + returns trigger + language plpgsql + strict as $$ +begin + call buildRbacSystemForHsBookingProject(NEW); + return NEW; +end; $$; + +create trigger insertTriggerForHsBookingProject_tg + after insert on hs_booking_project + for each row +execute procedure insertTriggerForHsBookingProject_tf(); +--// + + +-- ============================================================================ +--changeset hs-booking-project-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +-- granting INSERT permission to hs_office_relation ---------------------------- + +/* + 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_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_project'), + hsOfficeRelationADMIN(row)); + END LOOP; + end; +$$; + +/** + Grants hs_booking_project INSERT permission to specified role of new hs_office_relation rows. +*/ +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_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_project_grants_insert_to_hs_office_relation_tg + after insert on hs_office_relation + for each row +execute procedure new_hs_booking_project_grants_insert_to_hs_office_relation_tf(); + + +-- ============================================================================ +--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_project. +*/ +create or replace function hs_booking_project_insert_permission_check_tf() + returns trigger + language plpgsql as $$ +declare + superObjectUuid uuid; +begin + -- check INSERT permission via indirect foreign key: NEW.debitorUuid + superObjectUuid := (SELECT debitorRel.uuid + FROM hs_office_relation debitorRel + 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_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_project not allowed for current subjects % (%)', + currentSubjects(), currentSubjectsUuids(); +end; $$; + +create trigger hs_booking_project_insert_permission_check_tg + before insert on hs_booking_project + for each row + execute procedure hs_booking_project_insert_permission_check_tf(); +--// + + +-- ============================================================================ +--changeset hs-booking-project-rbac-IDENTITY-VIEW:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +call generateRbacIdentityViewFromQuery('hs_booking_project', + $idName$ + 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-project-rbac-RESTRICTED-VIEW:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- +call generateRbacRestrictedView('hs_booking_project', + $orderBy$ + caption + $orderBy$, + $updates$ + version = new.version, + caption = new.caption + $updates$); +--// + 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/610-booking-project/6108-hs-booking-project-test-data.sql new file mode 100644 index 00000000..5ebae299 --- /dev/null +++ b/src/main/resources/db/changelog/6-hs-booking/610-booking-project/6108-hs-booking-project-test-data.sql @@ -0,0 +1,51 @@ +--liquibase formatted sql + + +-- ============================================================================ +--changeset hs-booking-project-TEST-DATA-GENERATOR:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/* + Creates a single hs_booking_project test record. + */ +create or replace procedure createHsBookingProjectTransactionTestData( + givenPartnerNumber numeric, + givenDebitorSuffix char(2) + ) + language plpgsql as $$ +declare + currentTask varchar; + relatedDebitor hs_office_debitor; +begin + 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); + + 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; + + raise notice 'creating test booking-project: %', givenDebitorSuffix::text; + raise notice '- using debitor (%): %', relatedDebitor.uuid, relatedDebitor; + insert + into hs_booking_project (uuid, debitoruuid, caption) + values (uuid_generate_v4(), relatedDebitor.uuid, 'D-' || givenPartnerNumber::text || givenDebitorSuffix || ' default project'); +end; $$; +--// + + +-- ============================================================================ +--changeset hs-booking-project-TEST-DATA-GENERATION:1 –context=dev,tc endDelimiter:--// +-- ---------------------------------------------------------------------------- + +do language plpgsql $$ + begin + 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 93% 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..096b2600 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,7 +17,7 @@ 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 not null references hs_booking_project(uuid), type HsBookingItemType not null, validity daterange not null, caption varchar(80) 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..067241e4 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,34 +29,33 @@ 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:project: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:project:ADMIN ==> perm:bookingItem:INSERT role:global:ADMIN ==> perm:bookingItem:DELETE role:bookingItem:ADMIN ==> perm:bookingItem:UPDATE role:bookingItem:TENANT ==> perm:bookingItem: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/620-booking-item/6203-hs-booking-item-rbac.sql similarity index 70% 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/620-booking-item/6203-hs-booking-item-rbac.sql index e26edbbb..e0475e6b 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/620-booking-item/6203-hs-booking-item-rbac.sql @@ -30,26 +30,18 @@ create or replace procedure buildRbacSystemForHsBookingItem( language plpgsql as $$ declare - newDebitor hs_office_debitor; - newDebitorRel hs_office_relation; + newProject hs_booking_project; begin call enterTriggerForObjectUuid(NEW.uuid); - SELECT * FROM hs_office_debitor WHERE uuid = NEW.debitorUuid INTO newDebitor; - assert newDebitor.uuid is not null, format('newDebitor must not be null for NEW.debitorUuid = %s', NEW.debitorUuid); - - SELECT debitorRel.* - FROM hs_office_relation debitorRel - JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid - WHERE debitor.uuid = NEW.debitorUuid - INTO newDebitorRel; - assert newDebitorRel.uuid is not null, format('newDebitorRel must not be null for NEW.debitorUuid = %s', NEW.debitorUuid); + SELECT * FROM hs_booking_project WHERE uuid = NEW.projectUuid INTO newProject; + assert newProject.uuid is not null, format('newProject must not be null for NEW.projectUuid = %s', NEW.projectUuid); perform createRoleWithGrants( hsBookingItemOWNER(NEW), - incomingSuperRoles => array[hsOfficeRelationAGENT(newDebitorRel)] + incomingSuperRoles => array[hsBookingProjectAGENT(newProject)] ); perform createRoleWithGrants( @@ -57,7 +49,7 @@ begin permissions => array['UPDATE'], incomingSuperRoles => array[ hsBookingItemOWNER(NEW), - hsOfficeRelationAGENT(newDebitorRel)] + hsBookingProjectAGENT(newProject)] ); perform createRoleWithGrants( @@ -69,7 +61,7 @@ begin hsBookingItemTENANT(NEW), permissions => array['SELECT'], incomingSuperRoles => array[hsBookingItemAGENT(NEW)], - outgoingSubRoles => array[hsOfficeRelationTENANT(newDebitorRel)] + outgoingSubRoles => array[hsBookingProjectTENANT(newProject)] ); call grantPermissionToRole(createPermission(NEW.uuid, 'DELETE'), globalAdmin()); @@ -101,48 +93,48 @@ execute procedure insertTriggerForHsBookingItem_tf(); --changeset hs-booking-item-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--// -- ---------------------------------------------------------------------------- --- granting INSERT permission to hs_office_relation ---------------------------- +-- granting INSERT permission to hs_booking_project ---------------------------- /* - Grants INSERT INTO hs_booking_item permissions to specified role of pre-existing hs_office_relation rows. + Grants INSERT INTO hs_booking_item permissions to specified role of pre-existing hs_booking_project rows. */ do language plpgsql $$ declare - row hs_office_relation; + row hs_booking_project; begin - call defineContext('create INSERT INTO hs_booking_item permissions for pre-exising hs_office_relation rows'); + call defineContext('create INSERT INTO hs_booking_item permissions for pre-exising hs_booking_project rows'); - FOR row IN SELECT * FROM hs_office_relation - WHERE type = 'DEBITOR' + 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'), - hsOfficeRelationADMIN(row)); + hsBookingProjectADMIN(row)); END LOOP; end; $$; /** - Grants hs_booking_item INSERT permission to specified role of new hs_office_relation rows. + 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_office_relation_tf() +create or replace function new_hs_booking_item_grants_insert_to_hs_booking_project_tf() returns trigger language plpgsql strict as $$ begin - if NEW.type = 'DEBITOR' then + -- unconditional for all rows in that table call grantPermissionToRole( createPermission(NEW.uuid, 'INSERT', 'hs_booking_item'), - hsOfficeRelationADMIN(NEW)); - end if; + 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_office_relation_tg - after insert on hs_office_relation +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_office_relation_tf(); +execute procedure new_hs_booking_item_grants_insert_to_hs_booking_project_tf(); -- ============================================================================ @@ -158,14 +150,8 @@ create or replace function hs_booking_item_insert_permission_check_tf() declare superObjectUuid uuid; begin - -- check INSERT permission via indirect foreign key: NEW.debitorUuid - superObjectUuid := (SELECT debitorRel.uuid - FROM hs_office_relation debitorRel - 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 + -- check INSERT permission via direct foreign key: NEW.projectUuid + if hasInsertPermission(NEW.projectUuid, 'hs_booking_item') then return NEW; end if; @@ -186,9 +172,9 @@ create trigger hs_booking_item_insert_permission_check_tg call generateRbacIdentityViewFromQuery('hs_booking_item', $idName$ - SELECT bookingItem.uuid as uuid, debitorIV.idName || '-' || cleanIdentifier(bookingItem.caption) as idName + SELECT bookingItem.uuid as uuid, projectIV.idName || '-' || cleanIdentifier(bookingItem.caption) as idName FROM hs_booking_item bookingItem - JOIN hs_office_debitor_iv debitorIV ON debitorIV.uuid = bookingItem.debitorUuid + JOIN hs_booking_project_iv projectIV ON projectIV.uuid = bookingItem.projectUuid $idName$); --// 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/620-booking-item/6208-hs-booking-item-test-data.sql similarity index 66% 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/620-booking-item/6208-hs-booking-item-test-data.sql index 88ada16f..91aca115 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/620-booking-item/6208-hs-booking-item-test-data.sql @@ -15,26 +15,23 @@ create or replace procedure createHsBookingItemTransactionTestData( language plpgsql as $$ declare currentTask varchar; - relatedDebitor hs_office_debitor; + relatedProject hs_booking_project; 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 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 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 debitor (%): %', relatedDebitor.uuid, relatedDebitor; + raise notice '- using project (%): %', relatedProject.uuid, relatedProject; 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_item (uuid, projectuuid, type, caption, validity, resources) + values (uuid_generate_v4(), relatedProject.uuid, 'MANAGED_SERVER', 'some ManagedServer', daterange('20221001', null, '[]'), '{ "CPUs": 2, "RAM": 8, "SDD": 512, "Traffic": 42 }'::jsonb), + (uuid_generate_v4(), relatedProject.uuid, 'CLOUD_SERVER', 'some CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 2, "RAM": 4, "HDD": 1024, "Traffic": 42 }'::jsonb), + (uuid_generate_v4(), relatedProject.uuid, 'PRIVATE_CLOUD', 'some PrivateCloud', daterange('20240401', null, '[]'), '{ "CPUs": 10, "SDD": 10240, "HDD": 10240, "Traffic": 42 }'::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 4aa9e099..6609bbe8 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 @@ -28,10 +28,11 @@ create table if not exists hs_hosting_asset type HsHostingAssetType not null, parentAssetUuid uuid null references hs_hosting_asset(uuid), 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 (type in ('CLOUD_SERVER', 'MANAGED_SERVER') or 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..66472b8a 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 @@ -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 @@ -87,5 +66,6 @@ role:asset:TENANT ==> role:bookingItem:TENANT 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.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql index 2495f1ea..d1f8c163 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 @@ -162,6 +162,49 @@ create trigger z_new_hs_hosting_asset_grants_insert_to_hs_hosting_asset_tg for each row execute procedure new_hs_hosting_asset_grants_insert_to_hs_hosting_asset_tf(); +-- 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(); + -- ============================================================================ --changeset hs_hosting_asset-rbac-CHECKING-INSERT-PERMISSION:1 endDelimiter:--// @@ -184,6 +227,10 @@ begin if NEW.type in ('MANAGED_WEBSPACE') and hasInsertPermission(NEW.parentAssetUuid, 'hs_hosting_asset') then return NEW; end if; + -- check INSERT INSERT if global ADMIN + if isGlobalAdmin() then + return NEW; + end if; raise exception '[403] insert into hs_hosting_asset not allowed for current subjects % (%)', currentSubjects(), currentSubjectsUuids(); @@ -200,11 +247,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/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java index 0a92ff3f..b0d9794e 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,7 @@ 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.HsBookingProjectRepository; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; @@ -17,6 +18,7 @@ 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; @@ -39,6 +41,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup @Autowired HsBookingItemRepository bookingItemRepo; + @Autowired + HsBookingProjectRepository projectRepo; + @Autowired HsOfficeDebitorRepository debitorRepo; @@ -56,14 +61,18 @@ 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") @@ -118,7 +127,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 +139,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,7 +178,7 @@ 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(bi -> bi.getProject().getDebitor().getDebitorNumber() == 1000111) .filter(item -> item.getCaption().equals("some CloudServer")) .findAny().orElseThrow().getUuid(); @@ -197,7 +210,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 -> bi.getProject().getDebitor().getDebitorNumber() == 1000212) .map(HsBookingItemEntity::getUuid) .findAny().orElseThrow(); @@ -215,7 +228,7 @@ 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(bi -> bi.getProject().getDebitor().getDebitorNumber() == 1000313) .filter(item -> item.getCaption().equals("some CloudServer")) .findAny().orElseThrow().getUuid(); @@ -290,7 +303,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 +358,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..ce69ee98 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#D-1000111-D-1000111defaultproject-somenewbookingitem:ADMIN", + "hs_booking_item#D-1000111-D-1000111defaultproject-somenewbookingitem:AGENT", + "hs_booking_item#D-1000111-D-1000111defaultproject-somenewbookingitem:OWNER", + "hs_booking_item#D-1000111-D-1000111defaultproject-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#D-1000111-D-1000111defaultproject-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#D-1000111-D-1000111defaultproject-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#D-1000111-D-1000111defaultproject-somenewbookingitem:UPDATE to role:hs_booking_item#D-1000111-D-1000111defaultproject-somenewbookingitem:ADMIN by system and assume }", + "{ grant role:hs_booking_item#D-1000111-D-1000111defaultproject-somenewbookingitem:ADMIN to role:hs_booking_item#D-1000111-D-1000111defaultproject-somenewbookingitem:OWNER by system and assume }", + "{ grant perm:hs_booking_item#D-1000111-D-1000111defaultproject-somenewbookingitem:INSERT>hs_hosting_asset to role:hs_booking_item#D-1000111-D-1000111defaultproject-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#D-1000111-D-1000111defaultproject-somenewbookingitem:ADMIN to role:hs_booking_project#D-1000111-D-1000111defaultproject:AGENT by system and assume }", + "{ grant role:hs_booking_item#D-1000111-D-1000111defaultproject-somenewbookingitem:AGENT to role:hs_booking_item#D-1000111-D-1000111defaultproject-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#D-1000111-D-1000111defaultproject-somenewbookingitem:TENANT to role:hs_booking_item#D-1000111-D-1000111defaultproject-somenewbookingitem:AGENT by system and assume }", + "{ grant perm:hs_booking_item#D-1000111-D-1000111defaultproject-somenewbookingitem:SELECT to role:hs_booking_item#D-1000111-D-1000111defaultproject-somenewbookingitem:TENANT by system and assume }", + "{ grant role:hs_booking_project#D-1000111-D-1000111defaultproject:TENANT to role:hs_booking_item#D-1000111-D-1000111defaultproject-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,), some ManagedServer, { CPUs: 2, RAM: 8, SDD: 512, Traffic: 42 })", + "HsBookingItemEntity(D-1000212:D-1000212 default project, CLOUD_SERVER, [2023-01-15,2024-04-15), some CloudServer, { CPUs: 2, HDD: 1024, RAM: 4, Traffic: 42 })", + "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,), some ManagedServer, { CPUs: 2, RAM: 8, SDD: 512, Traffic: 42 })", + "HsBookingItemEntity(D-1000111:D-1000111 default project, CLOUD_SERVER, [2023-01-15,2024-04-15), some CloudServer, { CPUs: 2, HDD: 1024, RAM: 4, Traffic: 42 })", + "HsBookingItemEntity(D-1000111:D-1000111 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10240, SDD: 10240, Traffic: 42 })"); } } @@ -317,8 +327,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup return jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net"); final var givenDebitor = debitorRepo.findDebitorByDebitorNumber(debitorNumber).get(0); + final var givenProject = projectRepo.findAllByDebitorUuid(givenDebitor.getUuid()).get(0); 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..69cb83dd --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java @@ -0,0 +1,327 @@ +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:ADMIN to role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:AGENT by system and assume }", + "{ 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..cade487b 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.project.HsBookingProjectRepository; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; @@ -41,6 +42,9 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup @Autowired HsBookingItemRepository bookingItemRepo; + @Autowired + HsBookingProjectRepository projectRepo; + @Autowired HsOfficeDebitorRepository debitorRepo; @@ -55,14 +59,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()) .then().log().all().assertThat() .statusCode(200) .contentType("application/json") @@ -285,7 +291,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 +320,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 +338,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(); @@ -452,9 +458,11 @@ 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() + 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(); } 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..933cf468 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,27 @@ 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:ADMIN", + "hs_hosting_asset#vm9000:OWNER", + "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#D-1000111-D-1000111defaultproject-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:TENANT 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#D-1000111-D-1000111defaultproject-somePrivateCloud:TENANT to role:hs_hosting_asset#vm9000:TENANT by system and assume }", null)); } @@ -162,26 +166,30 @@ 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:some ManagedServer, { HDD: 2048, RAM: 1, SDD: 512, extra: 42 })", + "HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project: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:some 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(); + context("superuser-alex@hostsharing.net"); // FIXME + 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); + // FIXME generateRbacDiagramForCurrentSubjects(RbacGrantsDiagramService.Include.ALL_NON_TEST_ENTITY_RELATED, "normalUser_canViewOnlyRelatedAsset"); + 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:some 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 +205,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, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:some ManagedServer, { HDD: 2048, RAM: 1, SDD: 512, extra: 42 })"); } } @@ -208,7 +216,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 +250,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 +270,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 +292,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 +318,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 +348,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(projectCaption, "some CloudServer"); final var newAsset = HsHostingAssetEntity.builder() .bookingItem(givenBookingItem) .type(CLOUD_SERVER) @@ -363,16 +371,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/rbac/rbacrole/RawRbacRoleEntity.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacRoleEntity.java index e80f8ce6..bfd47e7c 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacRoleEntity.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacRoleEntity.java @@ -41,6 +41,11 @@ public class RawRbacRoleEntity { @NotNull public static List distinctRoleNamesOf(@NotNull final List roles) { // TODO: remove .distinct() once partner.person + partner.contract are removed + roles.forEach(r -> { + if (r.getRoleName() == null) { + r.toString(); + } + }); return roles.stream().map(RawRbacRoleEntity::getRoleName).sorted().distinct().toList(); }