From fe23a496e6c8b9b2d3a1833141bd2d301883a932 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Tue, 12 Mar 2024 17:36:29 +0100 Subject: [PATCH] test-data-generation working up to membership, fails in coop-shares --- .../HsOfficeMembershipController.java | 7 +- .../membership/HsOfficeMembershipEntity.java | 60 ++++- .../HsOfficeMembershipEntityPatcher.java | 17 -- .../db/changelog/300-hs-office-membership.sql | 1 - .../303-hs-office-membership-rbac.md | 203 ++++++++++----- .../303-hs-office-membership-rbac.sql | 236 +++++++++--------- .../308-hs-office-membership-test-data.sql | 26 +- ...iceMembershipControllerAcceptanceTest.java | 6 - ...OfficeMembershipEntityPatcherUnitTest.java | 18 +- .../HsOfficeMembershipEntityUnitTest.java | 2 - ...ceMembershipRepositoryIntegrationTest.java | 18 +- .../hs/office/migration/ImportOfficeData.java | 1 - src/test/resources/application.yml | 4 +- 13 files changed, 334 insertions(+), 265 deletions(-) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipController.java index e18fc183..540ba2a2 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipController.java @@ -12,8 +12,6 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; import jakarta.validation.Valid; import java.util.List; import java.util.UUID; @@ -32,9 +30,6 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi { @Autowired private HsOfficeMembershipRepository membershipRepo; - @PersistenceContext - private EntityManager em; - @Override @Transactional(readOnly = true) public ResponseEntity> listMemberships( @@ -121,7 +116,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi { final var current = membershipRepo.findByUuid(membershipUuid).orElseThrow(); - new HsOfficeMembershipEntityPatcher(em, mapper, current).apply(body); + new HsOfficeMembershipEntityPatcher(mapper, current).apply(body); final var saved = membershipRepo.save(current); final var mapped = mapper.map(saved, HsOfficeMembershipResource.class, SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java index a367935e..5afcd2c8 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntity.java @@ -4,20 +4,29 @@ import com.vladmihalcea.hibernate.type.range.PostgreSQLRangeType; import com.vladmihalcea.hibernate.type.range.Range; import lombok.*; import net.hostsharing.hsadminng.errors.DisplayName; -import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; +import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; +import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; +import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringifyable; -import org.hibernate.annotations.Fetch; -import org.hibernate.annotations.FetchMode; import org.hibernate.annotations.Type; import jakarta.persistence.*; +import java.io.IOException; import java.time.LocalDate; import java.util.UUID; import static net.hostsharing.hsadminng.mapper.PostgresDateRange.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.REFERRER; +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; @Entity @@ -35,7 +44,6 @@ public class HsOfficeMembershipEntity implements HasUuid, Stringifyable { private static Stringify stringify = stringify(HsOfficeMembershipEntity.class) .withProp(e -> MEMBER_NUMBER_TAG + e.getMemberNumber()) .withProp(e -> e.getPartner().toShortString()) - .withProp(e -> e.getMainDebitor().toShortString()) .withProp(e -> e.getValidity().asString()) .withProp(HsOfficeMembershipEntity::getReasonForTermination) .quotedValues(false); @@ -48,11 +56,6 @@ public class HsOfficeMembershipEntity implements HasUuid, Stringifyable { @JoinColumn(name = "partneruuid") private HsOfficePartnerEntity partner; - @ManyToOne - @Fetch(FetchMode.JOIN) - @JoinColumn(name = "maindebitoruuid") - private HsOfficeDebitorEntity mainDebitor; - @Column(name = "membernumbersuffix", length = 2) private String memberNumberSuffix; @@ -113,4 +116,43 @@ public class HsOfficeMembershipEntity implements HasUuid, Stringifyable { setReasonForTermination(HsOfficeReasonForTermination.NONE); } } + + public static RbacView rbac() { + return rbacViewFor("membership", HsOfficeMembershipEntity.class) + .withIdentityView(SQL.query(""" + SELECT m.uuid AS uuid, + 'M-' || p.partnerNumber || m.memberNumberSuffix as idName + FROM hs_office_membership AS m + JOIN hs_office_partner AS p ON p.uuid = m.partnerUuid + """)) + .withRestrictedViewOrderBy(SQL.projection("validity")) + .withUpdatableColumns("validity", "membershipFeeBillable", "reasonForTermination") + + .importEntityAlias("partnerRel", HsOfficeRelationshipEntity.class, + dependsOnColumn("partnerUuid"), + fetchedBySql(""" + SELECT r.* + FROM hs_office_partner AS p + JOIN hs_office_relationship AS r ON r.uuid = p.partnerRoleUuid + WHERE p.uuid = ${REF}.partnerUuid + """)) + .toRole("partnerRel", ADMIN).grantPermission("membership", INSERT) + + .createRole(OWNER, (with) -> { + with.owningUser(CREATOR); + with.incomingSuperRole("partnerRel", ADMIN); + with.permission(DELETE); + }) + .createSubRole(ADMIN, (with) -> { + with.permission(UPDATE); + }) + .createSubRole(REFERRER, (with) -> { + with.outgoingSubRole("partnerRel", TENANT); + with.permission(SELECT); + }); + } + + public static void main(String[] args) throws IOException { + rbac().generateWithBaseFileName("303-hs-office-membership-rbac"); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcher.java index 59fa6070..89933fe8 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcher.java @@ -1,37 +1,26 @@ package net.hostsharing.hsadminng.hs.office.membership; -import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource; import net.hostsharing.hsadminng.mapper.EntityPatcher; import net.hostsharing.hsadminng.mapper.Mapper; import net.hostsharing.hsadminng.mapper.OptionalFromJson; -import jakarta.persistence.EntityManager; import java.util.Optional; -import java.util.UUID; public class HsOfficeMembershipEntityPatcher implements EntityPatcher { - private final EntityManager em; private final Mapper mapper; private final HsOfficeMembershipEntity entity; public HsOfficeMembershipEntityPatcher( - final EntityManager em, final Mapper mapper, final HsOfficeMembershipEntity entity) { - this.em = em; this.mapper = mapper; this.entity = entity; } @Override public void apply(final HsOfficeMembershipPatchResource resource) { - OptionalFromJson.of(resource.getMainDebitorUuid()) - .ifPresent(newValue -> { - verifyNotNull(newValue, "debitor"); - entity.setMainDebitor(em.getReference(HsOfficeDebitorEntity.class, newValue)); - }); OptionalFromJson.of(resource.getValidTo()).ifPresent( entity::setValidTo); Optional.ofNullable(resource.getReasonForTermination()) @@ -40,10 +29,4 @@ public class HsOfficeMembershipEntityPatcher implements EntityPatcher role:hsOfficeDebitor.admin[debitor.admin] - --> role:hsOfficeDebitor.tenant[debitor.tenant] - --> role:hsOfficeDebitor.guest[debitor.guest] + style partnerRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph partnerRel.contact["`**partnerRel.contact**`"] + direction TB + style partnerRel.contact fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph partnerRel.contact:roles[ ] + style partnerRel.contact:roles fill:#99bcdb,stroke:white + + role:partnerRel.contact:owner[[partnerRel.contact:owner]] + role:partnerRel.contact:admin[[partnerRel.contact:admin]] + role:partnerRel.contact:referrer[[partnerRel.contact:referrer]] + end + end + + subgraph partnerRel.anchorPerson["`**partnerRel.anchorPerson**`"] + direction TB + style partnerRel.anchorPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph partnerRel.anchorPerson:roles[ ] + style partnerRel.anchorPerson:roles fill:#99bcdb,stroke:white + + role:partnerRel.anchorPerson:owner[[partnerRel.anchorPerson:owner]] + role:partnerRel.anchorPerson:admin[[partnerRel.anchorPerson:admin]] + role:partnerRel.anchorPerson:referrer[[partnerRel.anchorPerson:referrer]] + end + end + + subgraph partnerRel.holderPerson["`**partnerRel.holderPerson**`"] + direction TB + style partnerRel.holderPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph partnerRel.holderPerson:roles[ ] + style partnerRel.holderPerson:roles fill:#99bcdb,stroke:white + + role:partnerRel.holderPerson:owner[[partnerRel.holderPerson:owner]] + role:partnerRel.holderPerson:admin[[partnerRel.holderPerson:admin]] + role:partnerRel.holderPerson:referrer[[partnerRel.holderPerson:referrer]] + end + end + + subgraph partnerRel:roles[ ] + style partnerRel:roles fill:#99bcdb,stroke:white + + role:partnerRel:owner[[partnerRel:owner]] + role:partnerRel:admin[[partnerRel:admin]] + role:partnerRel:agent[[partnerRel:agent]] + role:partnerRel:tenant[[partnerRel:tenant]] + end end -subgraph hsOfficePartner +subgraph partnerRel.contact["`**partnerRel.contact**`"] direction TB - style hsOfficePartner fill:#eee - - role:hsOfficePartner.owner[partner.admin] - --> role:hsOfficePartner.admin[partner.admin] - --> role:hsOfficePartner.agent[partner.agent] - --> role:hsOfficePartner.tenant[partner.tenant] - --> role:hsOfficePartner.guest[partner.guest] + style partnerRel.contact fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph partnerRel.contact:roles[ ] + style partnerRel.contact:roles fill:#99bcdb,stroke:white + + role:partnerRel.contact:owner[[partnerRel.contact:owner]] + role:partnerRel.contact:admin[[partnerRel.contact:admin]] + role:partnerRel.contact:referrer[[partnerRel.contact:referrer]] + end end -subgraph hsOfficeMembership - - role:hsOfficeMembership.owner[membership.owner] - %% permissions - role:hsOfficeMembership.owner --> perm:hsOfficeMembership.*{{membership.*}} - %% incoming - role:global.admin ---> role:hsOfficeMembership.owner - - role:hsOfficeMembership.admin[membership.admin] - %% permissions - role:hsOfficeMembership.admin --> perm:hsOfficeMembership.edit{{membership.edit}} - %% incoming - role:hsOfficeMembership.owner ---> role:hsOfficeMembership.admin - - role:hsOfficeMembership.agent[membership.agent] - %% incoming - role:hsOfficeMembership.admin ---> role:hsOfficeMembership.agent - role:hsOfficePartner.admin --> role:hsOfficeMembership.agent - role:hsOfficeDebitor.admin --> role:hsOfficeMembership.agent - %% outgoing - role:hsOfficeMembership.agent --> role:hsOfficePartner.tenant - role:hsOfficeMembership.agent --> role:hsOfficeDebitor.tenant - - role:hsOfficeMembership.tenant[membership.tenant] - %% incoming - role:hsOfficeMembership.agent --> role:hsOfficeMembership.tenant - role:hsOfficePartner.agent --> role:hsOfficeMembership.tenant - role:hsOfficeDebitor.agent --> role:hsOfficeMembership.tenant - %% outgoing - role:hsOfficeMembership.tenant --> role:hsOfficePartner.guest - role:hsOfficeMembership.tenant --> role:hsOfficeDebitor.guest +subgraph membership["`**membership**`"] + direction TB + style membership fill:#dd4901,stroke:#274d6e,stroke-width:8px - role:hsOfficeMembership.guest[membership.guest] - %% permissions - role:hsOfficeMembership.guest --> perm:hsOfficeMembership.view{{membership.view}} - %% incoming - role:hsOfficeMembership.tenant --> role:hsOfficeMembership.guest - role:hsOfficePartner.tenant --> role:hsOfficeMembership.guest - role:hsOfficeDebitor.tenant --> role:hsOfficeMembership.guest + subgraph membership:roles[ ] + style membership:roles fill:#dd4901,stroke:white + + role:membership:owner[[membership:owner]] + role:membership:admin[[membership:admin]] + role:membership:referrer[[membership:referrer]] + end + + subgraph membership:permissions[ ] + style membership:permissions fill:#dd4901,stroke:white + + perm:membership:INSERT{{membership:INSERT}} + perm:membership:DELETE{{membership:DELETE}} + perm:membership:UPDATE{{membership:UPDATE}} + perm:membership:SELECT{{membership:SELECT}} + end end +subgraph partnerRel.anchorPerson["`**partnerRel.anchorPerson**`"] + direction TB + style partnerRel.anchorPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph partnerRel.anchorPerson:roles[ ] + style partnerRel.anchorPerson:roles fill:#99bcdb,stroke:white + + role:partnerRel.anchorPerson:owner[[partnerRel.anchorPerson:owner]] + role:partnerRel.anchorPerson:admin[[partnerRel.anchorPerson:admin]] + role:partnerRel.anchorPerson:referrer[[partnerRel.anchorPerson:referrer]] + end +end + +subgraph partnerRel.holderPerson["`**partnerRel.holderPerson**`"] + direction TB + style partnerRel.holderPerson fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph partnerRel.holderPerson:roles[ ] + style partnerRel.holderPerson:roles fill:#99bcdb,stroke:white + + role:partnerRel.holderPerson:owner[[partnerRel.holderPerson:owner]] + role:partnerRel.holderPerson:admin[[partnerRel.holderPerson:admin]] + role:partnerRel.holderPerson:referrer[[partnerRel.holderPerson:referrer]] + end +end + +%% granting roles to users +user:creator ==> role:membership:owner + +%% granting roles to roles +role:global:admin -.-> role:partnerRel.anchorPerson:owner +role:partnerRel.anchorPerson:owner -.-> role:partnerRel.anchorPerson:admin +role:partnerRel.anchorPerson:admin -.-> role:partnerRel.anchorPerson:referrer +role:global:admin -.-> role:partnerRel.holderPerson:owner +role:partnerRel.holderPerson:owner -.-> role:partnerRel.holderPerson:admin +role:partnerRel.holderPerson:admin -.-> role:partnerRel.holderPerson:referrer +role:global:admin -.-> role:partnerRel.contact:owner +role:partnerRel.contact:owner -.-> role:partnerRel.contact:admin +role:partnerRel.contact:admin -.-> role:partnerRel.contact:referrer +role:global:admin -.-> role:partnerRel:owner +role:partnerRel:owner -.-> role:partnerRel:admin +role:partnerRel.anchorPerson:admin -.-> role:partnerRel:admin +role:partnerRel:admin -.-> role:partnerRel:agent +role:partnerRel.holderPerson:admin -.-> role:partnerRel:agent +role:partnerRel:agent -.-> role:partnerRel:tenant +role:partnerRel.holderPerson:admin -.-> role:partnerRel:tenant +role:partnerRel.contact:admin -.-> role:partnerRel:tenant +role:partnerRel:tenant -.-> role:partnerRel.anchorPerson:referrer +role:partnerRel:tenant -.-> role:partnerRel.holderPerson:referrer +role:partnerRel:tenant -.-> role:partnerRel.contact:referrer +role:partnerRel:admin ==> role:membership:owner +role:membership:owner ==> role:membership:admin +role:membership:admin ==> role:membership:referrer +role:membership:referrer ==> role:partnerRel:tenant + +%% granting permissions to roles +role:global:admin ==> perm:membership:INSERT +role:membership:owner ==> perm:membership:DELETE +role:membership:admin ==> perm:membership:UPDATE +role:membership:referrer ==> perm:membership:SELECT ``` diff --git a/src/main/resources/db/changelog/303-hs-office-membership-rbac.sql b/src/main/resources/db/changelog/303-hs-office-membership-rbac.sql index 0dae225b..88c72c3a 100644 --- a/src/main/resources/db/changelog/303-hs-office-membership-rbac.sql +++ b/src/main/resources/db/changelog/303-hs-office-membership-rbac.sql @@ -1,4 +1,6 @@ --liquibase formatted sql +-- This code generated was by RbacViewPostgresGenerator at 2024-03-12T17:26:50.179864268. + -- ============================================================================ --changeset hs-office-membership-rbac-OBJECT:1 endDelimiter:--// @@ -15,150 +17,160 @@ call generateRbacRoleDescriptors('hsOfficeMembership', 'hs_office_membership'); -- ============================================================================ ---changeset hs-office-membership-rbac-ROLES-CREATION:1 endDelimiter:--// +--changeset hs-office-membership-rbac-insert-trigger:1 endDelimiter:--// -- ---------------------------------------------------------------------------- /* - Creates and updates the roles and their assignments for membership entities. + Creates the roles, grants and permission for the AFTER INSERT TRIGGER. */ -create or replace function hsOfficeMembershipRbacRolesTrigger() - returns trigger - language plpgsql - strict as $$ +create or replace procedure buildRbacSystemForHsOfficeMembership( + NEW hs_office_membership +) + language plpgsql as $$ + declare - newHsOfficePartner hs_office_partner; - newHsOfficePartnerRel hs_office_relationship; - newHsOfficeDebitor hs_office_debitor; + newPartnerRel hs_office_relationship; + begin call enterTriggerForObjectUuid(NEW.uuid); - select * from hs_office_partner as p where p.uuid = NEW.partnerUuid into newHsOfficePartner; - select * from hs_office_relationship as r where r.relType = 'PARTNER' and r.relHolderUuid = NEW.partnerUuid into newHsOfficePartnerRel; - select * from hs_office_debitor as c where c.uuid = NEW.mainDebitorUuid into newHsOfficeDebitor; + SELECT r.* + FROM hs_office_partner AS p + JOIN hs_office_relationship AS r ON r.uuid = p.partnerRoleUuid + WHERE p.uuid = NEW.partnerUuid + INTO newPartnerRel; + assert newPartnerRel.uuid is not null, format('newPartnerRel must not be null for NEW.partnerUuid = %s', NEW.partnerUuid); - if TG_OP = 'INSERT' then - -- === ATTENTION: code generated from related Mermaid flowchart: === + perform createRoleWithGrants( + hsOfficeMembershipOwner(NEW), + permissions => array['DELETE'], + userUuids => array[currentUserUuid()], + incomingSuperRoles => array[hsOfficeRelationshipAdmin(newPartnerRel)] + ); - perform createRoleWithGrants( - hsOfficeMembershipOwner(NEW), - permissions => array['DELETE'], - incomingSuperRoles => array[globalAdmin()] - ); + perform createRoleWithGrants( + hsOfficeMembershipAdmin(NEW), + permissions => array['UPDATE'], + incomingSuperRoles => array[hsOfficeMembershipOwner(NEW)] + ); - perform createRoleWithGrants( - hsOfficeMembershipAdmin(NEW), - permissions => array['UPDATE'], - incomingSuperRoles => array[hsOfficeMembershipOwner(NEW)] - ); - - perform createRoleWithGrants( - hsOfficeMembershipAgent(NEW), - incomingSuperRoles => array[hsOfficeMembershipAdmin(NEW), hsOfficeRelationshipAdmin(newHsOfficePartnerRel), hsOfficeDebitorAdmin(newHsOfficeDebitor)], - outgoingSubRoles => array[hsOfficeRelationshipTenant(newHsOfficePartnerRel), hsOfficeDebitorTenant(newHsOfficeDebitor)] - ); - - perform createRoleWithGrants( - hsOfficeMembershipTenant(NEW), - incomingSuperRoles => array[hsOfficeMembershipAgent(NEW), hsOfficeRelationshipAgent(newHsOfficePartnerRel), hsOfficeDebitorAgent(newHsOfficeDebitor)], - outgoingSubRoles => array[hsOfficeRelationshipGuest(newHsOfficePartnerRel), hsOfficeDebitorGuest(newHsOfficeDebitor)] - ); - - perform createRoleWithGrants( - hsOfficeMembershipGuest(NEW), - permissions => array['SELECT'], - incomingSuperRoles => array[hsOfficeMembershipTenant(NEW), hsOfficeRelationshipTenant(newHsOfficePartnerRel), hsOfficeDebitorTenant(newHsOfficeDebitor)] - ); - - -- === END of code generated from Mermaid flowchart. === - - else - raise exception 'invalid usage of TRIGGER'; - end if; + perform createRoleWithGrants( + hsOfficeMembershipReferrer(NEW), + permissions => array['SELECT'], + incomingSuperRoles => array[hsOfficeMembershipAdmin(NEW)], + outgoingSubRoles => array[hsOfficeRelationshipTenant(newPartnerRel)] + ); call leaveTriggerForObjectUuid(NEW.uuid); - return NEW; end; $$; /* - An AFTER INSERT TRIGGER which creates the role structure for a new customer. + AFTER INSERT TRIGGER to create the role+grant structure for a new hs_office_membership row. */ -create trigger createRbacRolesForHsOfficeMembership_Trigger - after insert - on hs_office_membership + +create or replace function insertTriggerForHsOfficeMembership_tf() + returns trigger + language plpgsql + strict as $$ +begin + call buildRbacSystemForHsOfficeMembership(NEW); + return NEW; +end; $$; + +create trigger insertTriggerForHsOfficeMembership_tg + after insert on hs_office_membership for each row -execute procedure hsOfficeMembershipRbacRolesTrigger(); +execute procedure insertTriggerForHsOfficeMembership_tf(); --// -- ============================================================================ ---changeset hs-office-membership-rbac-IDENTITY-VIEW:1 endDelimiter:--// +--changeset hs-office-membership-rbac-INSERT:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityViewFromProjection('hs_office_membership', $idName$ - '#' || - (select partnerNumber from hs_office_partner p where p.uuid = target.partnerUuid) || - memberNumberSuffix || - ':' || (select split_part(idName, ':', 2) from hs_office_partner_iv p where p.uuid = target.partnerUuid) - $idName$); + +/* + Creates INSERT INTO hs_office_membership permissions for the related global rows. + */ +do language plpgsql $$ + declare + row global; + permissionUuid uuid; + roleUuid uuid; + begin + call defineContext('create INSERT INTO hs_office_membership permissions for the related global rows'); + + FOR row IN SELECT * FROM global + LOOP + roleUuid := findRoleId(globalAdmin()); + permissionUuid := createPermission(row.uuid, 'INSERT', 'hs_office_membership'); + call grantPermissionToRole(permissionUuid, roleUuid); + END LOOP; + END; +$$; + +/** + Adds hs_office_membership INSERT permission to specified role of new global rows. +*/ +create or replace function hs_office_membership_global_insert_tf() + returns trigger + language plpgsql + strict as $$ +begin + call grantPermissionToRole( + globalAdmin(), + createPermission(NEW.uuid, 'INSERT', 'hs_office_membership')); + return NEW; +end; $$; + +create trigger hs_office_membership_global_insert_tg + after insert on global + for each row +execute procedure hs_office_membership_global_insert_tf(); + +/** + Checks if the user or assumed roles are allowed to insert a row to hs_office_membership. +*/ +create or replace function hs_office_membership_insert_permission_missing_tf() + returns trigger + language plpgsql as $$ +begin + raise exception '[403] insert into hs_office_membership not allowed for current subjects % (%)', + currentSubjects(), currentSubjectsUuids(); +end; $$; + +create trigger hs_office_membership_insert_permission_check_tg + before insert on hs_office_membership + for each row + when ( not isGlobalAdmin() ) + execute procedure hs_office_membership_insert_permission_missing_tf(); --// +-- ============================================================================ +--changeset hs-office-membership-rbac-IDENTITY-VIEW:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + + call generateRbacIdentityViewFromQuery('hs_office_membership', $idName$ + SELECT m.uuid AS uuid, + 'M-' || p.partnerNumber || m.memberNumberSuffix as idName +FROM hs_office_membership AS m +JOIN hs_office_partner AS p ON p.uuid = m.partnerUuid + + $idName$); +--// -- ============================================================================ --changeset hs-office-membership-rbac-RESTRICTED-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- call generateRbacRestrictedView('hs_office_membership', - orderby => 'target.memberNumberSuffix', - columnUpdates => $updates$ - validity = new.validity, - reasonForTermination = new.reasonForTermination, - membershipFeeBillable = new.membershipFeeBillable + $orderBy$ + validity + $orderBy$, + $updates$ + validity = new.validity, + membershipFeeBillable = new.membershipFeeBillable, + reasonForTermination = new.reasonForTermination $updates$); --// - --- ============================================================================ ---changeset hs-office-membership-rbac-NEW-Membership:1 endDelimiter:--// --- ---------------------------------------------------------------------------- -/* - Creates a global permission for new-membership and assigns it to the hostsharing admins role. - */ -do language plpgsql $$ - declare - addCustomerPermissions uuid[]; - globalObjectUuid uuid; - globalAdminRoleUuid uuid ; - begin - call defineContext('granting global new-membership permission to global admin role', null, null, null); - - globalAdminRoleUuid := findRoleId(globalAdmin()); - globalObjectUuid := (select uuid from global); - addCustomerPermissions := createPermissions(globalObjectUuid, array ['new-membership']); - call grantPermissionsToRole(globalAdminRoleUuid, addCustomerPermissions); - end; -$$; - -/** - Used by the trigger to prevent the add-customer to current user respectively assumed roles. - */ -create or replace function addHsOfficeMembershipNotAllowedForCurrentSubjects() - returns trigger - language PLPGSQL -as $$ -begin - raise exception '[403] new-membership not permitted for %', - array_to_string(currentSubjects(), ';', 'null'); -end; $$; - -/** - Checks if the user or assumed roles are allowed to create a new customer. - */ -create trigger hs_office_membership_insert_trigger - before insert - on hs_office_membership - for each row - -- TODO.spec: who is allowed to create new memberships - when ( not hasAssumedRole() ) -execute procedure addHsOfficeMembershipNotAllowedForCurrentSubjects(); ---// - diff --git a/src/main/resources/db/changelog/308-hs-office-membership-test-data.sql b/src/main/resources/db/changelog/308-hs-office-membership-test-data.sql index f7e945ac..9d574a58 100644 --- a/src/main/resources/db/changelog/308-hs-office-membership-test-data.sql +++ b/src/main/resources/db/changelog/308-hs-office-membership-test-data.sql @@ -10,42 +10,26 @@ */ create or replace procedure createHsOfficeMembershipTestData( forPartnerNumber numeric(5), - forMainDebitorNumberSuffix numeric, newMemberNumberSuffix char(2) ) language plpgsql as $$ declare currentTask varchar; relatedPartner hs_office_partner; - relatedDebitorRel hs_office_relationship; - relatedDebitor hs_office_debitor; begin currentTask := 'creating Membership test-data ' || 'P-' || forPartnerNumber::text || - 'D-...' || forMainDebitorNumberSuffix || 'M-...' || newMemberNumberSuffix; call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global.admin'); execute format('set local hsadminng.currentTask to %L', currentTask); select partner.* from hs_office_partner partner where partner.partnerNumber = forPartnerNumber into relatedPartner; - select debitorRel.* from hs_office_relationship debitorRel - join hs_office_relationship partnerRel - on debitorRel.relAnchorUuid=partnerRel.relHolderUuid and partnerRel.relType='PARTNER' - join hs_office_partner partner - on partner.partnerRoleUuid = partnerRel.uuid - where debitorRel.relType='ACCOUNTING' -- FIXME: 'DEBITOR' - into relatedDebitorRel; - select debitor.* from hs_office_debitor debitor - where debitor.debitorRelUuid = relatedDebitorRel.uuid - and debitor.debitorNumberSuffix = forMainDebitorNumberSuffix - into relatedDebitor; raise notice 'creating test Membership: M-% %', forPartnerNumber, newMemberNumberSuffix; raise notice '- using partner (%): %', relatedPartner.uuid, relatedPartner; - raise notice '- using debitor (%): %', relatedDebitor.uuid, relatedDebitor; insert - into hs_office_membership (uuid, partneruuid, maindebitoruuid, memberNumberSuffix, validity, reasonfortermination) - values (uuid_generate_v4(), relatedPartner.uuid, relatedDebitor.uuid, newMemberNumberSuffix, daterange('20221001' , null, '[]'), 'NONE'); + into hs_office_membership (uuid, partneruuid, memberNumberSuffix, validity, reasonfortermination) + values (uuid_generate_v4(), relatedPartner.uuid, newMemberNumberSuffix, daterange('20221001' , null, '[]'), 'NONE'); end; $$; --// @@ -56,9 +40,9 @@ end; $$; do language plpgsql $$ begin - call createHsOfficeMembershipTestData(10001, 11, '01'); - call createHsOfficeMembershipTestData(10002, 12, '02'); - call createHsOfficeMembershipTestData(10003, 13, '03'); + call createHsOfficeMembershipTestData(10001, '01'); + call createHsOfficeMembershipTestData(10002, '02'); + call createHsOfficeMembershipTestData(10003, '03'); end; $$; --// diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerAcceptanceTest.java index bcd0bd26..ff53d482 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipControllerAcceptanceTest.java @@ -334,9 +334,6 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle .contentType(ContentType.JSON) .body("uuid", isUuidValid()) .body("partner.person.tradeName", is(givenMembership.getPartner().getPartnerRole().getRelHolder().getTradeName())) - .body("mainDebitor.debitorNumber", is(givenMembership.getMainDebitor().getDebitorNumber())) - .body("mainDebitor.debitorNumberSuffix", is((int) givenMembership.getMainDebitor().getDebitorNumberSuffix())) - .body("mainDebitor.debitorNumberSuffix", is((int) givenMembership.getMainDebitor().getDebitorNumberSuffix())) .body("memberNumberSuffix", is(givenMembership.getMemberNumberSuffix())) .body("validFrom", is("2022-11-01")) .body("validTo", is("2023-12-31")) @@ -347,7 +344,6 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle assertThat(membershipRepo.findByUuid(givenMembership.getUuid())).isPresent().get() .matches(mandate -> { assertThat(mandate.getPartner().toShortString()).isEqualTo("LP First GmbH"); - assertThat(mandate.getMainDebitor().toString()).isEqualTo(givenMembership.getMainDebitor().toString()); assertThat(mandate.getMemberNumberSuffix()).isEqualTo(givenMembership.getMemberNumberSuffix()); assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,2024-01-01)"); assertThat(mandate.getReasonForTermination()).isEqualTo(HsOfficeReasonForTermination.CANCELLATION); @@ -390,7 +386,6 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle assertThat(membershipRepo.findByUuid(givenMembership.getUuid())).isPresent().get() .matches(mandate -> { assertThat(mandate.getPartner().toShortString()).isEqualTo("LP First GmbH"); - assertThat(mandate.getMainDebitor().toString()).isEqualTo(givenMembership.getMainDebitor().toString()); assertThat(mandate.getMemberNumberSuffix()).isEqualTo(givenMembership.getMemberNumberSuffix()); assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,)"); assertThat(mandate.getReasonForTermination()).isEqualTo(NONE); @@ -501,7 +496,6 @@ class HsOfficeMembershipControllerAcceptanceTest extends ContextBasedTestWithCle final var newMembership = HsOfficeMembershipEntity.builder() .uuid(UUID.randomUUID()) .partner(givenPartner) - .mainDebitor(givenDebitor) .memberNumberSuffix(TEMP_MEMBER_NUMBER_SUFFIX) .validity(Range.closedInfinite(LocalDate.parse("2022-11-01"))) .reasonForTermination(NONE) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcherUnitTest.java index ee4944c1..b691095b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcherUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityPatcherUnitTest.java @@ -17,7 +17,6 @@ import java.time.LocalDate; 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.office.partner.TestHsOfficePartner.TEST_PARTNER; import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; import static org.mockito.ArgumentMatchers.any; @@ -32,7 +31,6 @@ class HsOfficeMembershipEntityPatcherUnitTest extends PatchUnitTestBase< > { private static final UUID INITIAL_MEMBERSHIP_UUID = UUID.randomUUID(); - private static final UUID PATCHED_MAIN_DEBITOR_UUID = UUID.randomUUID(); private static final LocalDate GIVEN_VALID_FROM = LocalDate.parse("2020-04-15"); private static final LocalDate PATCHED_VALID_TO = LocalDate.parse("2022-12-31"); @@ -56,7 +54,6 @@ class HsOfficeMembershipEntityPatcherUnitTest extends PatchUnitTestBase< protected HsOfficeMembershipEntity newInitialEntity() { final var entity = new HsOfficeMembershipEntity(); entity.setUuid(INITIAL_MEMBERSHIP_UUID); - entity.setMainDebitor(TEST_DEBITOR); entity.setPartner(TEST_PARTNER); entity.setValidity(Range.closedInfinite(GIVEN_VALID_FROM)); entity.setMembershipFeeBillable(GIVEN_MEMBERSHIP_FEE_BILLABLE); @@ -70,19 +67,12 @@ class HsOfficeMembershipEntityPatcherUnitTest extends PatchUnitTestBase< @Override protected HsOfficeMembershipEntityPatcher createPatcher(final HsOfficeMembershipEntity membership) { - return new HsOfficeMembershipEntityPatcher(em, mapper, membership); + return new HsOfficeMembershipEntityPatcher(mapper, membership); } @Override protected Stream propertyTestDescriptors() { return Stream.of( - new JsonNullableProperty<>( - "debitor", - HsOfficeMembershipPatchResource::setMainDebitorUuid, - PATCHED_MAIN_DEBITOR_UUID, - HsOfficeMembershipEntity::setMainDebitor, - newDebitor(PATCHED_MAIN_DEBITOR_UUID)) - .notNullable(), new JsonNullableProperty<>( "valid", HsOfficeMembershipPatchResource::setValidTo, @@ -102,10 +92,4 @@ class HsOfficeMembershipEntityPatcherUnitTest extends PatchUnitTestBase< HsOfficeMembershipEntity::setMembershipFeeBillable) ); } - - private static HsOfficeDebitorEntity newDebitor(final UUID uuid) { - final var newDebitor = new HsOfficeDebitorEntity(); - newDebitor.setUuid(uuid); - return newDebitor; - } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityUnitTest.java index b1815755..aeac829b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipEntityUnitTest.java @@ -9,7 +9,6 @@ import java.lang.reflect.InvocationTargetException; import java.time.LocalDate; import java.util.Arrays; -import static net.hostsharing.hsadminng.hs.office.debitor.TestHsOfficeDebitor.TEST_DEBITOR; import static net.hostsharing.hsadminng.hs.office.partner.TestHsOfficePartner.TEST_PARTNER; import static org.assertj.core.api.Assertions.assertThat; @@ -20,7 +19,6 @@ class HsOfficeMembershipEntityUnitTest { final HsOfficeMembershipEntity givenMembership = HsOfficeMembershipEntity.builder() .memberNumberSuffix("01") .partner(TEST_PARTNER) - .mainDebitor(TEST_DEBITOR) .validity(Range.closedInfinite(GIVEN_VALID_FROM)) .build(); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java index 4483304a..88df5541 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java @@ -28,6 +28,7 @@ import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distin import static net.hostsharing.hsadminng.rbac.rbacrole.RawRbacRoleEntity.distinctRoleNamesOf; import static net.hostsharing.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; + @DataJpaTest @Import( { Context.class, JpaAttempt.class }) class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCleanup { @@ -72,7 +73,6 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl final var newMembership = HsOfficeMembershipEntity.builder() .memberNumberSuffix("11") .partner(givenPartner) - .mainDebitor(givenDebitor) .validity(Range.closedInfinite(LocalDate.parse("2020-01-01"))) .membershipFeeBillable(true) .build(); @@ -99,11 +99,9 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl // when attempt(em, () -> { final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("First").get(0); - final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike("First").get(0); final var newMembership = HsOfficeMembershipEntity.builder() .memberNumberSuffix("17") .partner(givenPartner) - .mainDebitor(givenDebitor) .validity(Range.closedInfinite(LocalDate.parse("2020-01-01"))) .membershipFeeBillable(true) .build(); @@ -219,7 +217,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl public void globalAdmin_canUpdateValidityOfArbitraryMembership() { // given context("superuser-alex@hostsharing.net"); - final var givenMembership = givenSomeTemporaryMembership("First", "First", "11"); + final var givenMembership = givenSomeTemporaryMembership("First", "11"); assertThatMembershipIsVisibleForUserWithRole( givenMembership, "hs_office_debitor#1000111:FirstGmbH-firstcontact.admin"); @@ -246,7 +244,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl public void debitorAdmin_canViewButNotUpdateRelatedMembership() { // given context("superuser-alex@hostsharing.net"); - final var givenMembership = givenSomeTemporaryMembership("First", "First", "13"); + final var givenMembership = givenSomeTemporaryMembership("First", "13"); assertThatMembershipIsVisibleForUserWithRole( givenMembership, "hs_office_debitor#1000111:FirstGmbH-firstcontact.admin"); @@ -299,7 +297,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl public void globalAdmin_withoutAssumedRole_canDeleteAnyMembership() { // given context("superuser-alex@hostsharing.net", null); - final var givenMembership = givenSomeTemporaryMembership("First", "Second", "12"); + final var givenMembership = givenSomeTemporaryMembership("First", "12"); // when final var result = jpaAttempt.transacted(() -> { @@ -319,7 +317,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl public void nonGlobalAdmin_canNotDeleteTheirRelatedMembership() { // given context("superuser-alex@hostsharing.net"); - final var givenMembership = givenSomeTemporaryMembership("First", "Third", "14"); + final var givenMembership = givenSomeTemporaryMembership("First", "14"); // when final var result = jpaAttempt.transacted(() -> { @@ -345,7 +343,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl context("superuser-alex@hostsharing.net"); final var initialRoleNames = Array.from(distinctRoleNamesOf(rawRoleRepo.findAll())); final var initialGrantNames = Array.from(distinctGrantDisplaysOf(rawGrantRepo.findAll())); - final var givenMembership = givenSomeTemporaryMembership("First", "First", "15"); + final var givenMembership = givenSomeTemporaryMembership("First", "15"); assertThat(distinctRoleNamesOf(rawRoleRepo.findAll()).size()).as("precondition failed: unexpected number of roles created") .isEqualTo(initialRoleNames.length + 5); assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll()).size()).as("precondition failed: unexpected number of grants created") @@ -383,15 +381,13 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl "[creating Membership test-data Seconde.K.12, hs_office_membership, INSERT]"); } - private HsOfficeMembershipEntity givenSomeTemporaryMembership(final String partnerTradeName, final String debitorName, final String memberNumberSuffix) { + private HsOfficeMembershipEntity givenSomeTemporaryMembership(final String partnerTradeName, final String memberNumberSuffix) { return jpaAttempt.transacted(() -> { context("superuser-alex@hostsharing.net"); final var givenPartner = partnerRepo.findPartnerByOptionalNameLike(partnerTradeName).get(0); - final var givenDebitor = debitorRepo.findDebitorByOptionalNameLike(debitorName).get(0); final var newMembership = HsOfficeMembershipEntity.builder() .memberNumberSuffix(memberNumberSuffix) .partner(givenPartner) - .mainDebitor(givenDebitor) .validity(Range.closedInfinite(LocalDate.parse("2020-01-01"))) .membershipFeeBillable(true) .build(); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java index c5db82e4..5853e6f0 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java @@ -704,7 +704,6 @@ public class ImportOfficeData extends ContextBasedTest { isBlank(rec.getString("member_until")) ? HsOfficeReasonForTermination.NONE : HsOfficeReasonForTermination.UNKNOWN) - .mainDebitor(debitor) .build(); memberships.put(rec.getInteger("bp_id"), membership); } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 01e283b9..a4f570f9 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -4,8 +4,8 @@ spring: platform: postgres datasource: - url-tc: jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers - url: jdbc:postgresql://localhost:5432/postgres + url: jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers + url-local: jdbc:postgresql://localhost:5432/postgres username: postgres password: password