From d9558f2cfe71b90faffb803b22ce475dc645e550 Mon Sep 17 00:00:00 2001
From: Michael Hoennig <michael.hoennig@hostsharing.net>
Date: Sat, 24 Feb 2024 09:04:07 +0100
Subject: [PATCH] add-trigger-object-to-rbacgrant (#18)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/18
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
---
 .../resources/db/changelog/050-rbac-base.sql  | 21 ++++---
 .../resources/db/changelog/055-rbac-views.sql |  2 +
 .../db/changelog/056-rbac-trigger-context.sql | 61 +++++++++++++++++++
 .../db/changelog/113-test-customer-rbac.sql   |  3 +
 .../db/changelog/123-test-package-rbac.sql    |  5 +-
 .../db/changelog/133-test-domain-rbac.sql     |  3 +
 .../223-hs-office-relationship-rbac.sql       |  2 +
 .../changelog/233-hs-office-partner-rbac.sql  |  2 +
 .../253-hs-office-sepamandate-rbac.sql        |  2 +
 .../changelog/273-hs-office-debitor-rbac.sql  |  2 +
 .../303-hs-office-membership-rbac.sql         |  2 +
 .../313-hs-office-coopshares-rbac.sql         |  2 +
 .../323-hs-office-coopassets-rbac.sql         |  2 +
 .../db/changelog/db.changelog-master.yaml     |  2 +
 .../hs/office/migration/ImportOfficeData.java | 35 +++++------
 ...RelationshipRepositoryIntegrationTest.java |  4 --
 src/test/resources/migration/contacts.csv     |  2 +-
 17 files changed, 116 insertions(+), 36 deletions(-)
 create mode 100644 src/main/resources/db/changelog/056-rbac-trigger-context.sql

diff --git a/src/main/resources/db/changelog/050-rbac-base.sql b/src/main/resources/db/changelog/050-rbac-base.sql
index 40c15646..fe2f30ae 100644
--- a/src/main/resources/db/changelog/050-rbac-base.sql
+++ b/src/main/resources/db/changelog/050-rbac-base.sql
@@ -467,12 +467,13 @@ end $$;
 create table RbacGrants
 (
     uuid                uuid primary key default uuid_generate_v4(),
+    grantedByTriggerOf  uuid references RbacObject (uuid) on delete cascade initially deferred ,
     grantedByRoleUuid   uuid references RbacRole (uuid),
     ascendantUuid       uuid references RbacReference (uuid),
     descendantUuid      uuid references RbacReference (uuid),
     assumed             boolean not null default true,  -- auto assumed (true) vs. needs assumeRoles (false)
-    unique (ascendantUuid, descendantUuid)
-);
+    unique (ascendantUuid, descendantUuid),
+    constraint rbacGrant_createdBy check ( grantedByRoleUuid is null or grantedByTriggerOf is null) );
 create index on RbacGrants (ascendantUuid);
 create index on RbacGrants (descendantUuid);
 
@@ -576,8 +577,8 @@ begin
             perform assertReferenceType('permissionId (descendant)', permissionIds[i], 'RbacPermission');
 
             insert
-                into RbacGrants (ascendantUuid, descendantUuid, assumed)
-                values (roleUuid, permissionIds[i], true)
+                into RbacGrants (grantedByTriggerOf, ascendantUuid, descendantUuid, assumed)
+                values (currentTriggerObjectUuid(), roleUuid, permissionIds[i], true)
             on conflict do nothing; -- allow granting multiple times
         end loop;
 end;
@@ -594,8 +595,8 @@ begin
     end if;
 
     insert
-        into RbacGrants (ascendantuuid, descendantUuid, assumed)
-        values (superRoleId, subRoleId, doAssume)
+        into RbacGrants (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed)
+        values (currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume)
     on conflict do nothing; -- allow granting multiple times
 end; $$;
 
@@ -617,8 +618,8 @@ begin
     end if;
 
     insert
-        into RbacGrants (ascendantuuid, descendantUuid, assumed)
-        values (superRoleId, subRoleId, doAssume)
+        into RbacGrants (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed)
+        values (currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume)
     on conflict do nothing; -- allow granting multiple times
 end; $$;
 
@@ -640,8 +641,8 @@ begin
     end if;
 
     insert
-        into RbacGrants (ascendantuuid, descendantUuid, assumed)
-        values (superRoleId, subRoleId, doAssume)
+        into RbacGrants (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed)
+        values (currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume)
     on conflict do nothing; -- allow granting multiple times
 end; $$;
 
diff --git a/src/main/resources/db/changelog/055-rbac-views.sql b/src/main/resources/db/changelog/055-rbac-views.sql
index d1d1d926..b1757c56 100644
--- a/src/main/resources/db/changelog/055-rbac-views.sql
+++ b/src/main/resources/db/changelog/055-rbac-views.sql
@@ -56,6 +56,7 @@ drop view if exists rbacgrants_ev;
 create or replace view rbacgrants_ev as
     -- @formatter:off
     select x.grantUuid as uuid,
+           x.grantedByTriggerOf as grantedByTriggerOf,
            go.objectTable || '#' || findIdNameByObjectUuid(go.objectTable, go.uuid) || '.' || r.roletype as grantedByRoleIdName,
            x.ascendingIdName as ascendantIdName,
            x.descendingIdName as descendantIdName,
@@ -65,6 +66,7 @@ create or replace view rbacgrants_ev as
            x.assumed
         from (
              select g.uuid as grantUuid,
+                    g.grantedbytriggerof as grantedbytriggerof,
                     g.grantedbyroleuuid, g.ascendantuuid, g.descendantuuid, g.assumed,
 
                     coalesce(
diff --git a/src/main/resources/db/changelog/056-rbac-trigger-context.sql b/src/main/resources/db/changelog/056-rbac-trigger-context.sql
new file mode 100644
index 00000000..80a92987
--- /dev/null
+++ b/src/main/resources/db/changelog/056-rbac-trigger-context.sql
@@ -0,0 +1,61 @@
+--liquibase formatted sql
+
+
+-- ============================================================================
+--changeset rbac-trigger-context-ENTER:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+create or replace procedure enterTriggerForObjectUuid(currentObjectUuid uuid)
+    language plpgsql as $$
+declare
+    existingObjectUuid text;
+begin
+    existingObjectUuid = current_setting('hsadminng.currentObjectUuid', true);
+    if (existingObjectUuid > '' ) then
+        raise exception '[500] currentObjectUuid already defined, already in trigger of "%"', existingObjectUuid;
+    end if;
+    execute format('set local hsadminng.currentObjectUuid to %L', currentObjectUuid);
+end; $$;
+
+
+-- ============================================================================
+--changeset rbac-trigger-context-CURRENT-ID:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+/*
+    Returns the uuid of the object uuid whose trigger is currently executed as set via `enterTriggerForObjectUuid(...)`.
+ */
+
+create or replace function currentTriggerObjectUuid()
+    returns uuid
+    stable -- leakproof
+    language plpgsql as $$
+declare
+    currentObjectUuid uuid;
+begin
+    begin
+        currentObjectUuid = current_setting('hsadminng.currentObjectUuid')::uuid;
+        return currentObjectUuid;
+    exception
+        when others then
+            return null::uuid;
+    end;
+end; $$;
+--//
+
+
+-- ============================================================================
+--changeset rbac-trigger-context-LEAVE:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+
+create or replace procedure leaveTriggerForObjectUuid(currentObjectUuid uuid)
+    language plpgsql as $$
+declare
+    existingObjectUuid uuid;
+begin
+    existingObjectUuid = current_setting('hsadminng.currentObjectUuid', true);
+    if ( existingObjectUuid <> currentObjectUuid ) then
+        raise exception '[500] currentObjectUuid does not match: "%"', existingObjectUuid;
+    end if;
+    execute format('reset hsadminng.currentObjectUuid');
+end; $$;
+
diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.sql b/src/main/resources/db/changelog/113-test-customer-rbac.sql
index 1f563aa2..d7682cc1 100644
--- a/src/main/resources/db/changelog/113-test-customer-rbac.sql
+++ b/src/main/resources/db/changelog/113-test-customer-rbac.sql
@@ -34,6 +34,8 @@ begin
         raise exception 'invalid usage of TRIGGER AFTER INSERT';
     end if;
 
+    call enterTriggerForObjectUuid(NEW.uuid);
+
     -- the owner role with full access for Hostsharing administrators
     testCustomerOwnerUuid = createRoleWithGrants(
         testCustomerOwner(NEW),
@@ -59,6 +61,7 @@ begin
         permissions =>  array['view']
         );
 
+    call leaveTriggerForObjectUuid(NEW.uuid);
     return NEW;
 end; $$;
 
diff --git a/src/main/resources/db/changelog/123-test-package-rbac.sql b/src/main/resources/db/changelog/123-test-package-rbac.sql
index 8a2fd857..9e68468c 100644
--- a/src/main/resources/db/changelog/123-test-package-rbac.sql
+++ b/src/main/resources/db/changelog/123-test-package-rbac.sql
@@ -26,13 +26,13 @@ create or replace function createRbacRolesForTestPackage()
     strict as $$
 declare
     parentCustomer       test_customer;
-    packageOwnerRoleUuid uuid;
-    packageAdminRoleUuid uuid;
 begin
     if TG_OP <> 'INSERT' then
         raise exception 'invalid usage of TRIGGER AFTER INSERT';
     end if;
 
+    call enterTriggerForObjectUuid(NEW.uuid);
+
     select * from test_customer as c where c.uuid = NEW.customerUuid into parentCustomer;
 
     -- an owner role is created and assigned to the customer's admin role
@@ -57,6 +57,7 @@ begin
             outgoingSubRoles => array[testCustomerTenant(parentCustomer)]
         );
 
+    call leaveTriggerForObjectUuid(NEW.uuid);
     return NEW;
 end; $$;
 
diff --git a/src/main/resources/db/changelog/133-test-domain-rbac.sql b/src/main/resources/db/changelog/133-test-domain-rbac.sql
index 89b63018..a78bfb5f 100644
--- a/src/main/resources/db/changelog/133-test-domain-rbac.sql
+++ b/src/main/resources/db/changelog/133-test-domain-rbac.sql
@@ -53,6 +53,8 @@ begin
         raise exception 'invalid usage of TRIGGER AFTER INSERT';
     end if;
 
+    call enterTriggerForObjectUuid(NEW.uuid);
+
     select * from test_package where uuid = NEW.packageUuid into parentPackage;
 
     -- an owner role is created and assigned to the package's admin group
@@ -72,6 +74,7 @@ begin
 
     -- a tenent role is only created on demand
 
+    call leaveTriggerForObjectUuid(NEW.uuid);
     return NEW;
 end; $$;
 
diff --git a/src/main/resources/db/changelog/223-hs-office-relationship-rbac.sql b/src/main/resources/db/changelog/223-hs-office-relationship-rbac.sql
index 03b0b748..928af48c 100644
--- a/src/main/resources/db/changelog/223-hs-office-relationship-rbac.sql
+++ b/src/main/resources/db/changelog/223-hs-office-relationship-rbac.sql
@@ -33,6 +33,7 @@ declare
     oldContact                  hs_office_contact;
     newContact                  hs_office_contact;
 begin
+    call enterTriggerForObjectUuid(NEW.uuid);
 
     hsOfficeRelationshipTenant := hsOfficeRelationshipTenant(NEW);
 
@@ -96,6 +97,7 @@ begin
         raise exception 'invalid usage of TRIGGER';
     end if;
 
+    call leaveTriggerForObjectUuid(NEW.uuid);
     return NEW;
 end; $$;
 
diff --git a/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql b/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql
index d4b0105c..4b4da009 100644
--- a/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql
+++ b/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql
@@ -36,6 +36,7 @@ declare
     oldContact            hs_office_contact;
     newContact            hs_office_contact;
 begin
+    call enterTriggerForObjectUuid(NEW.uuid);
 
     select * from hs_office_relationship as r where r.uuid = NEW.partnerroleuuid into newPartnerRole;
     select * from hs_office_person as p where p.uuid = NEW.personUuid into newPerson;
@@ -159,6 +160,7 @@ begin
         raise exception 'invalid usage of TRIGGER';
     end if;
 
+    call leaveTriggerForObjectUuid(NEW.uuid);
     return NEW;
 end; $$;
 
diff --git a/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql b/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql
index f09f2a4b..02895c48 100644
--- a/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql
+++ b/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql
@@ -30,6 +30,7 @@ declare
     newHsOfficeDebitor      hs_office_debitor;
     newHsOfficeBankAccount  hs_office_bankAccount;
 begin
+    call enterTriggerForObjectUuid(NEW.uuid);
 
     select * from hs_office_debitor as p where p.uuid = NEW.debitorUuid into newHsOfficeDebitor;
     select * from hs_office_bankAccount as c where c.uuid = NEW.bankAccountUuid into newHsOfficeBankAccount;
@@ -75,6 +76,7 @@ begin
         raise exception 'invalid usage of TRIGGER';
     end if;
 
+    call leaveTriggerForObjectUuid(NEW.uuid);
     return NEW;
 end; $$;
 
diff --git a/src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql b/src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql
index e6572e55..30573125 100644
--- a/src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql
+++ b/src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql
@@ -36,6 +36,7 @@ declare
     newBankAccount        hs_office_bankaccount;
     oldBankAccount        hs_office_bankaccount;
 begin
+    call enterTriggerForObjectUuid(NEW.uuid);
 
     hsOfficeDebitorTenant := hsOfficeDebitorTenant(NEW);
 
@@ -145,6 +146,7 @@ begin
         raise exception 'invalid usage of TRIGGER';
     end if;
 
+    call leaveTriggerForObjectUuid(NEW.uuid);
     return NEW;
 end; $$;
 
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 8197cf09..949f939c 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
@@ -30,6 +30,7 @@ declare
     newHsOfficePartner  hs_office_partner;
     newHsOfficeDebitor  hs_office_debitor;
 begin
+    call enterTriggerForObjectUuid(NEW.uuid);
 
     select * from hs_office_partner as p where p.uuid = NEW.partnerUuid into newHsOfficePartner;
     select * from hs_office_debitor as c where c.uuid = NEW.mainDebitorUuid into newHsOfficeDebitor;
@@ -74,6 +75,7 @@ begin
         raise exception 'invalid usage of TRIGGER';
     end if;
 
+    call leaveTriggerForObjectUuid(NEW.uuid);
     return NEW;
 end; $$;
 
diff --git a/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql b/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql
index d6afcfc8..dd465d9f 100644
--- a/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql
+++ b/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql
@@ -29,6 +29,7 @@ create or replace function hsOfficeCoopSharesTransactionRbacRolesTrigger()
 declare
     newHsOfficeMembership      hs_office_membership;
 begin
+    call enterTriggerForObjectUuid(NEW.uuid);
 
     select * from hs_office_membership as p where p.uuid = NEW.membershipUuid into newHsOfficeMembership;
 
@@ -49,6 +50,7 @@ begin
         raise exception 'invalid usage of TRIGGER';
     end if;
 
+    call leaveTriggerForObjectUuid(NEW.uuid);
     return NEW;
 end; $$;
 
diff --git a/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql b/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql
index 6589eaa2..ac65c141 100644
--- a/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql
+++ b/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql
@@ -29,6 +29,7 @@ create or replace function hsOfficeCoopAssetsTransactionRbacRolesTrigger()
 declare
     newHsOfficeMembership      hs_office_membership;
 begin
+    call enterTriggerForObjectUuid(NEW.uuid);
 
     select * from hs_office_membership as p where p.uuid = NEW.membershipUuid into newHsOfficeMembership;
 
@@ -49,6 +50,7 @@ begin
         raise exception 'invalid usage of TRIGGER';
     end if;
 
+    call leaveTriggerForObjectUuid(NEW.uuid);
     return NEW;
 end; $$;
 
diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml
index fdd04507..2b8417c3 100644
--- a/src/main/resources/db/changelog/db.changelog-master.yaml
+++ b/src/main/resources/db/changelog/db.changelog-master.yaml
@@ -25,6 +25,8 @@ databaseChangeLog:
         file: db/changelog/054-rbac-context.sql
     - include:
         file: db/changelog/055-rbac-views.sql
+    - include:
+        file: db/changelog/056-rbac-trigger-context.sql
     - include:
         file: db/changelog/057-rbac-role-builder.sql
     - include:
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 562eaf06..325317b2 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
@@ -278,20 +278,19 @@ public class ImportOfficeData extends ContextBasedTest {
                     2000002=rel(relAnchor='LP Hostsharing eG', relType='PARTNER', relHolder='?? Test PS', contact='Petra Schmidt , Test PS'),
                     2000003=rel(relAnchor='LP Hostsharing eG', relType='PARTNER', relHolder='null null, null'),
                     2000004=rel(relAnchor='NP Mellies, Michael', relType='OPERATIONS', relHolder='NP Mellies, Michael', contact='Herr Michael Mellies '),
-                    2000005=rel(relAnchor='LP JM GmbH', relType='EX_PARTNER', relHolder='LP JM e.K.', contact='JM e.K.'),
-                    2000006=rel(relAnchor='LP JM GmbH', relType='OPERATIONS', relHolder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'),
-                    2000007=rel(relAnchor='LP JM GmbH', relType='VIP_CONTACT', relHolder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'),
-                    2000008=rel(relAnchor='LP JM GmbH', relType='SUBSCRIBER', relMark='operations-announce', relHolder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'),
-                    2000009=rel(relAnchor='LP JM GmbH', relType='REPRESENTATIVE', relHolder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'),
-                    2000010=rel(relAnchor='LP JM GmbH', relType='SUBSCRIBER', relMark='members-announce', relHolder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'),
-                    2000011=rel(relAnchor='LP JM GmbH', relType='SUBSCRIBER', relMark='customers-announce', relHolder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'),
-                    2000012=rel(relAnchor='LP JM GmbH', relType='VIP_CONTACT', relHolder='LP JM GmbH', contact='Frau Tammy Meyer-VIP , JM GmbH'),
-                    2000013=rel(relAnchor='?? Test PS', relType='OPERATIONS', relHolder='?? Test PS', contact='Petra Schmidt , Test PS'),
-                    2000014=rel(relAnchor='?? Test PS', relType='REPRESENTATIVE', relHolder='?? Test PS', contact='Petra Schmidt , Test PS'),
-                    2000015=rel(relAnchor='NP Mellies, Michael', relType='SUBSCRIBER', relMark='operations-announce', relHolder='NP Fanninga, Frauke', contact='Frau Frauke Fanninga '),
-                    2000016=rel(relAnchor='NP Mellies, Michael', relType='REPRESENTATIVE', relHolder='NP Mellies, Michael', contact='Herr Michael Mellies '),
-                    2000017=rel(relAnchor='null null, null', relType='REPRESENTATIVE', relHolder='null null, null')
-                }
+                    2000005=rel(relAnchor='NP Mellies, Michael', relType='REPRESENTATIVE', relHolder='NP Mellies, Michael', contact='Herr Michael Mellies '),
+                    2000006=rel(relAnchor='LP JM GmbH', relType='EX_PARTNER', relHolder='LP JM e.K.', contact='JM e.K.'),
+                    2000007=rel(relAnchor='LP JM GmbH', relType='OPERATIONS', relHolder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'),
+                    2000008=rel(relAnchor='LP JM GmbH', relType='VIP_CONTACT', relHolder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'),
+                    2000009=rel(relAnchor='LP JM GmbH', relType='SUBSCRIBER', relMark='operations-announce', relHolder='LP JM GmbH', contact='Herr Andrew Meyer-Operation , JM GmbH'),
+                    2000010=rel(relAnchor='LP JM GmbH', relType='REPRESENTATIVE', relHolder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'),
+                    2000011=rel(relAnchor='LP JM GmbH', relType='SUBSCRIBER', relMark='members-announce', relHolder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'),
+                    2000012=rel(relAnchor='LP JM GmbH', relType='SUBSCRIBER', relMark='customers-announce', relHolder='LP JM GmbH', contact='Herr Philip Meyer-Contract , JM GmbH'),
+                    2000013=rel(relAnchor='LP JM GmbH', relType='VIP_CONTACT', relHolder='LP JM GmbH', contact='Frau Tammy Meyer-VIP , JM GmbH'),
+                    2000014=rel(relAnchor='?? Test PS', relType='OPERATIONS', relHolder='?? Test PS', contact='Petra Schmidt , Test PS'),
+                    2000015=rel(relAnchor='?? Test PS', relType='REPRESENTATIVE', relHolder='?? Test PS', contact='Petra Schmidt , Test PS'),
+                    2000016=rel(relAnchor='NP Mellies, Michael', relType='SUBSCRIBER', relMark='operations-announce', relHolder='NP Fanninga, Frauke', contact='Frau Frauke Fanninga ')
+                 }
                 """);
     }
 
@@ -412,7 +411,7 @@ public class ImportOfficeData extends ContextBasedTest {
                 idsToRemove.add(id);
             }
         });
-        assertThat(idsToRemove.size()).isEqualTo(2); // only from partner #99 (partner+contractual roles)
+        assertThat(idsToRemove.size()).isEqualTo(1); // only from partner #99 (partner+contractual roles)
         idsToRemove.forEach(id -> relationships.remove(id));
     }
 
@@ -421,7 +420,7 @@ public class ImportOfficeData extends ContextBasedTest {
     void removeEmptyPartners() {
         assumeThatWeAreImportingControlledTestData();
 
-        // avoid a error when persisting the deliberetely invalid partner entry #99
+        // avoid a error when persisting the deliberately invalid partner entry #99
         final var idsToRemove = new HashSet<Integer>();
         partners.forEach( (id, r) -> {
             // such a record
@@ -439,7 +438,7 @@ public class ImportOfficeData extends ContextBasedTest {
     void removeEmptyDebitors() {
         assumeThatWeAreImportingControlledTestData();
 
-        // avoid a error when persisting the deliberetely invalid partner entry #99
+        // avoid a error when persisting the deliberately invalid partner entry #99
         final var idsToRemove = new HashSet<Integer>();
         debitors.forEach( (id, r) -> {
             // such a record
@@ -881,11 +880,9 @@ public class ImportOfficeData extends ContextBasedTest {
             if (relationships.values().stream()
                     .filter(rel -> rel.getRelAnchor() == partnerPerson && rel.getRelType() == HsOfficeRelationshipType.REPRESENTATIVE)
                     .findFirst().isEmpty()) {
-                //addRelationship(partnerPerson, partnerPerson, partner.getContact(), HsOfficeRelationshipType.REPRESENTATIVE);
                 contractualMissing.add(partner.getPartnerNumber());
             }
         });
-        assertThat(contractualMissing).isEmpty(); // comment out if we do want to allow missing contractual contact
     }
     private static boolean containsRole(final Record rec, final String role) {
         final var roles = rec.getString("roles");
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipRepositoryIntegrationTest.java
index 8b732d66..8d89479c 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipRepositoryIntegrationTest.java
@@ -342,10 +342,6 @@ class HsOfficeRelationshipRepositoryIntegrationTest extends ContextBasedTestWith
             final var initialGrantNames = Array.from(distinctGrantDisplaysOf(rawGrantRepo.findAll()));
             final var givenRelationship = givenSomeTemporaryRelationshipBessler(
                     "Anita", "twelfth");
-            assertThat(distinctRoleNamesOf(rawRoleRepo.findAll()).size()).as("unexpected number of roles created")
-                    .isEqualTo(initialRoleNames.length + 3);
-            assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll()).size()).as("unexpected number of grants created")
-                    .isEqualTo(initialGrantNames.length + 13);
 
             // when
             final var result = jpaAttempt.transacted(() -> {
diff --git a/src/test/resources/migration/contacts.csv b/src/test/resources/migration/contacts.csv
index 0984c0d5..3aa1aa04 100644
--- a/src/test/resources/migration/contacts.csv
+++ b/src/test/resources/migration/contacts.csv
@@ -1,7 +1,7 @@
 contact_id;	bp_id;	salut;	first_name;	last_name;	title;	firma;	co;	street;			zipcode;city;	country;	phone_private;		phone_office;		phone_mobile;	fax;		email; roles
 
 # eine natürliche Person, implizites contractual
-1101;		17;	Herr;	Michael;	Mellies;		;	;	;	Kleine Freiheit 50;	26524;	Hage;	DE;		;			+49 4931 123456;	+49 1522 123456;;		mih@example.org; partner,billing,operation
+1101;		17;	Herr;	Michael;	Mellies;		;	;	;	Kleine Freiheit 50;	26524;	Hage;	DE;		;			+49 4931 123456;	+49 1522 123456;;		mih@example.org; partner,contractual,billing,operation
 
 # eine juristische Person mit drei separaten Ansprechpartnern, vip-contact und ex-partner
 1200;		20;;	;		;		;	JM e.K.;;	Wiesenweg 15;		12335;	Berlin; DE;		+49 30 6666666;		+49 30 5555555;		;		+49 30 6666666; jm-ex-partner@example.org; ex-partner