From 2138b3eed019229dc4a94325a28b93e7e9a88d84 Mon Sep 17 00:00:00 2001
From: Michael Hoennig <michael.hoennig@hostsharing.net>
Date: Thu, 15 Aug 2024 10:38:43 +0200
Subject: [PATCH] fix-domain-setup-rbac-grant-problems (#88)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/88
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
---
 ...e-cte-experiments-for-accessible-uuids.sql | 142 ++++++++++++++++++
 .../hs/booking/item/HsBookingItemEntity.java  |   4 +-
 .../project/HsBookingProjectEntity.java       |   2 +-
 .../hosting/asset/HsHostingAssetEntity.java   |   1 +
 .../hs/hosting/asset/HsHostingAssetType.java  |   2 +-
 .../rbacgrant/RbacGrantsDiagramService.java   |   2 +-
 .../changelog/0-basis/008-raise-functions.sql |  19 ++-
 .../changelog/1-rbac/1058-rbac-generators.sql |  49 +++---
 .../6203-hs-booking-project-rbac.md           |   2 +-
 .../6203-hs-booking-project-rbac.sql          |   2 +-
 .../7013-hs-hosting-asset-rbac.md             |   2 +
 .../7013-hs-hosting-asset-rbac.sql            |   4 +-
 .../7018-hs-hosting-asset-test-data.sql       |   7 +-
 ...HsBookingItemControllerAcceptanceTest.java |   3 +-
 .../item/HsBookingItemEntityUnitTest.java     |   2 +-
 ...sBookingItemRepositoryIntegrationTest.java |  24 +--
 ...ookingProjectControllerAcceptanceTest.java |   3 +-
 ...okingProjectRepositoryIntegrationTest.java |  10 +-
 ...sHostingAssetControllerAcceptanceTest.java |   7 +-
 ...HostingAssetRepositoryIntegrationTest.java |  32 +++-
 .../hs/migration/ImportHostingAssets.java     |  80 +++++++---
 .../resources/migration/hosting/inet_addr.csv |   1 +
 .../resources/migration/hosting/packet.csv    |   1 +
 .../migration/hosting/packet_component.csv    |   1 +
 .../resources/migration/hosting/unixuser.csv  |   1 +
 25 files changed, 317 insertions(+), 86 deletions(-)
 create mode 100644 sql/recursive-cte-experiments-for-accessible-uuids.sql

diff --git a/sql/recursive-cte-experiments-for-accessible-uuids.sql b/sql/recursive-cte-experiments-for-accessible-uuids.sql
new file mode 100644
index 00000000..f8795961
--- /dev/null
+++ b/sql/recursive-cte-experiments-for-accessible-uuids.sql
@@ -0,0 +1,142 @@
+-- just a permanent playground to explore optimization of the central recursive CTE query for RBAC
+
+rollback transaction;
+begin transaction;
+SET TRANSACTION READ ONLY;
+call defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
+                   'hs_booking_project#D-1000000-hshdefaultproject:ADMIN');
+--                    'hs_booking_project#D-1000300-mihdefaultproject:ADMIN');
+select count(type) as counter, type from hs_hosting_asset_rv
+    group by type
+    order by counter desc;
+commit transaction;
+
+
+
+
+rollback transaction;
+begin transaction;
+SET TRANSACTION READ ONLY;
+call defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
+     'hs_booking_project#D-1000000-hshdefaultproject:ADMIN');
+--                    'hs_booking_project#D-1000300-mihdefaultproject:ADMIN');
+
+with accessible_hs_hosting_asset_uuids as
+         (with recursive
+              recursive_grants as
+                  (select distinct rbacgrants.descendantuuid,
+                                   rbacgrants.ascendantuuid,
+                                   1 as level,
+                                   true
+                       from rbacgrants
+                       where rbacgrants.assumed
+                         and (rbacgrants.ascendantuuid = any (currentsubjectsuuids()))
+                   union all
+                   select distinct g.descendantuuid,
+                                   g.ascendantuuid,
+                                   grants.level + 1 as level,
+                                   assertTrue(grants.level < 22, 'too many grant-levels: ' || grants.level)
+                       from rbacgrants g
+                                join recursive_grants grants on grants.descendantuuid = g.ascendantuuid
+                       where g.assumed),
+              grant_count AS (
+                SELECT COUNT(*) AS grant_count FROM recursive_grants
+              ),
+              count_check as (select assertTrue((select count(*) as grant_count from recursive_grants) < 300000,
+                    'too many grants for current subjects: ' || (select count(*) as grant_count from recursive_grants))
+                                         as valid)
+          select distinct perm.objectuuid
+              from recursive_grants
+                       join rbacpermission perm on recursive_grants.descendantuuid = perm.uuid
+                       join rbacobject obj on obj.uuid = perm.objectuuid
+                       join count_check cc on cc.valid
+              where obj.objecttable::text = 'hs_hosting_asset'::text)
+select type,
+--        count(*) as counter
+       target.uuid,
+--        target.version,
+--        target.bookingitemuuid,
+--        target.type,
+--        target.parentassetuuid,
+--        target.assignedtoassetuuid,
+       target.identifier,
+       target.caption
+--        target.config,
+--        target.alarmcontactuuid
+    from hs_hosting_asset target
+    where (target.uuid in (select accessible_hs_hosting_asset_uuids.objectuuid
+                               from accessible_hs_hosting_asset_uuids))
+        and target.type in ('EMAIL_ADDRESS', 'CLOUD_SERVER', 'MANAGED_SERVER', 'MANAGED_WEBSPACE')
+--         and target.type = 'EMAIL_ADDRESS'
+--     order by target.identifier;
+--     group by type
+--     order by counter desc
+;
+commit transaction;
+
+
+
+
+rollback transaction;
+begin transaction;
+SET TRANSACTION READ ONLY;
+call defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
+                   'hs_booking_project#D-1000000-hshdefaultproject:ADMIN');
+--                    'hs_booking_project#D-1000300-mihdefaultproject:ADMIN');
+
+with one_path as (with recursive path as (
+        -- Base case: Start with the row where ascending equals the starting UUID
+        select ascendantuuid,
+               descendantuuid,
+               array [ascendantuuid] as path_so_far
+            from rbacgrants
+            where ascendantuuid = any (currentsubjectsuuids())
+
+        union all
+
+        -- Recursive case: Find the next step in the path
+        select c.ascendantuuid,
+               c.descendantuuid,
+               p.path_so_far || c.ascendantuuid
+            from rbacgrants c
+                     inner join
+                 path p on c.ascendantuuid = p.descendantuuid
+            where c.ascendantuuid != all (p.path_so_far) -- Prevent cycles
+    )
+      -- Final selection: Output all paths that reach the target UUID
+      select distinct array_length(path_so_far, 1),
+          path_so_far || descendantuuid as full_path
+          from path
+                   join rbacpermission perm on perm.uuid = path.descendantuuid
+                   join hs_hosting_asset ha on ha.uuid = perm.objectuuid
+      --    JOIN rbacrole_ev re on re.uuid = any(path_so_far)
+          where ha.identifier = 'vm1068'
+          order by array_length(path_so_far, 1)
+          limit 1
+  )
+select
+    (
+        SELECT ARRAY_AGG(re.roleidname ORDER BY ord.idx)
+            FROM UNNEST(one_path.full_path) WITH ORDINALITY AS ord(uuid, idx)
+                     JOIN rbacrole_ev re ON ord.uuid = re.uuid
+    ) AS name_array
+    from one_path;
+commit transaction;
+
+with grants as (
+    select uuid
+        from rbacgrants
+        where descendantuuid in (
+            select uuid
+                from rbacrole
+                where objectuuid in (
+                    select uuid
+                        from hs_hosting_asset
+                    --  where type = 'DOMAIN_MBOX_SETUP'
+                    --  and identifier = 'example.org|MBOX'
+                        where type = 'EMAIL_ADDRESS'
+                          and identifier='test@example.org'
+                ))
+)
+select * from rbacgrants_ev gev where exists ( select uuid from grants where gev.uuid = grants.uuid );
+
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 58a5d4b8..81c87e03 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
@@ -74,10 +74,10 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
 public class HsBookingItemEntity implements Stringifyable, BaseEntity<HsBookingItemEntity>, PropertiesProvider {
 
     private static Stringify<HsBookingItemEntity> stringify = stringify(HsBookingItemEntity.class)
-            .withProp(HsBookingItemEntity::getProject)
             .withProp(HsBookingItemEntity::getType)
-            .withProp(e -> e.getValidity().asString())
             .withProp(HsBookingItemEntity::getCaption)
+            .withProp(HsBookingItemEntity::getProject)
+            .withProp(e -> e.getValidity().asString())
             .withProp(HsBookingItemEntity::getResources)
             .quotedValues(false);
 
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntity.java
index c44d43f5..1d893ac0 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntity.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectEntity.java
@@ -94,7 +94,7 @@ public class HsBookingProjectEntity implements Stringifyable, BaseEntity<HsBooki
                 .toRole("global", ADMIN).grantPermission(DELETE)
 
                 .createRole(OWNER, (with) -> {
-                    with.incomingSuperRole("debitorRel", AGENT);
+                    with.incomingSuperRole("debitorRel", AGENT).unassumed();
                 })
                 .createSubRole(ADMIN, (with) -> {
                     with.permission(UPDATE);
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 46b315ff..2ae4ae70 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
@@ -185,6 +185,7 @@ public class HsHostingAssetEntity implements HsHostingAsset {
                     with.permission(UPDATE);
                 })
                 .createSubRole(AGENT, (with) -> {
+                    with.incomingSuperRole("assignedToAsset", AGENT); // TODO.spec: or ADMIN?
                     with.outgoingSubRole("assignedToAsset", TENANT);
                     with.outgoingSubRole("alarmContact", REFERRER);
                 })
diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java
index f08248c4..e11b1430 100644
--- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java
+++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java
@@ -264,7 +264,7 @@ public enum HsHostingAssetType implements Node {
                 package Booking #feb28c {
                 %{bookingNodes}
                 }
-                                
+
                 package Hosting #feb28c{
                 %{hostingGroups}
                 }
diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java
index fd33f358..f1369067 100644
--- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java
+++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java
@@ -215,7 +215,7 @@ public class RbacGrantsDiagramService {
     @NotNull
     private static String cleanId(final String idName) {
         return idName.replaceAll("@.*", "")
-                .replace("[", "").replace("]", "").replace("(", "").replace(")", "").replace(",", "").replace(">", ":");
+                .replace("[", "").replace("]", "").replace("(", "").replace(")", "").replace(",", "").replace(">", ":").replace("|", "_");
     }
 
 
diff --git a/src/main/resources/db/changelog/0-basis/008-raise-functions.sql b/src/main/resources/db/changelog/0-basis/008-raise-functions.sql
index 15b34d7d..ad298dc9 100644
--- a/src/main/resources/db/changelog/0-basis/008-raise-functions.sql
+++ b/src/main/resources/db/changelog/0-basis/008-raise-functions.sql
@@ -1,11 +1,10 @@
 --liquibase formatted sql
 
 -- ============================================================================
--- RAISE-FUNCTIONS
 --changeset RAISE-FUNCTIONS:1 endDelimiter:--//
 -- ----------------------------------------------------------------------------
 /*
-    Like RAISE EXCEPTION ... just as an expression instead of a statement.
+    Like `RAISE EXCEPTION` ... just as an expression instead of a statement.
  */
 create or replace function raiseException(msg text)
     returns varchar
@@ -14,3 +13,19 @@ begin
     raise exception using message = msg;
 end; $$;
 --//
+
+
+-- ============================================================================
+--changeset ASSERT-FUNCTIONS:1 endDelimiter:--//
+-- ----------------------------------------------------------------------------
+/*
+    Like `ASSERT` but as an expression instead of a statement.
+ */
+create or replace function assertTrue(expectedTrue boolean, msg text)
+    returns boolean
+    language plpgsql as $$
+begin
+    assert expectedTrue, msg;
+    return expectedTrue;
+end; $$;
+--//
diff --git a/src/main/resources/db/changelog/1-rbac/1058-rbac-generators.sql b/src/main/resources/db/changelog/1-rbac/1058-rbac-generators.sql
index 59223d9d..44281bed 100644
--- a/src/main/resources/db/changelog/1-rbac/1058-rbac-generators.sql
+++ b/src/main/resources/db/changelog/1-rbac/1058-rbac-generators.sql
@@ -177,26 +177,35 @@ begin
     sql := format($sql$
         create or replace view %1$s_rv as
             with accessible_%1$s_uuids as (
-
-                -- TODO.perf: this CTE query makes RBAC-SELECT-permission-queries so slow (~500ms), any idea how to optimize?
-                --  My guess is, that the depth of role-grants causes the problem.
-                with recursive grants as (
-                    select descendantUuid, ascendantUuid, 1 as level
-                        from RbacGrants
-                        where assumed
-                          and ascendantUuid = any (currentSubjectsuUids())
-                    union all
-                    select g.descendantUuid, g.ascendantUuid, level + 1 as level
-                        from RbacGrants g
-                                 inner join grants on grants.descendantUuid = g.ascendantUuid
-                        where g.assumed and level<10
-                )
-                select distinct perm.objectUuid as objectUuid
-                    from grants
-                             join RbacPermission perm on grants.descendantUuid = perm.uuid
-                             join RbacObject obj on obj.uuid = perm.objectUuid
-                    where obj.objectTable = '%1$s' -- 'SELECT' permission is included in all other permissions
-                    limit 8001
+                     with recursive
+                          recursive_grants as
+                              (select distinct rbacgrants.descendantuuid,
+                                               rbacgrants.ascendantuuid,
+                                               1 as level,
+                                               true
+                                   from rbacgrants
+                                   where rbacgrants.assumed
+                                     and (rbacgrants.ascendantuuid = any (currentsubjectsuuids()))
+                               union all
+                               select distinct g.descendantuuid,
+                                               g.ascendantuuid,
+                                               grants.level + 1 as level,
+                                               assertTrue(grants.level < 22, 'too many grant-levels: ' || grants.level)
+                                   from rbacgrants g
+                                            join recursive_grants grants on grants.descendantuuid = g.ascendantuuid
+                                   where g.assumed),
+                          grant_count AS (
+                            SELECT COUNT(*) AS grant_count FROM recursive_grants
+                          ),
+                          count_check as (select assertTrue((select count(*) as grant_count from recursive_grants) < 400000,
+                                'too many grants for current subjects: ' || (select count(*) as grant_count from recursive_grants))
+                                                     as valid)
+                      select distinct perm.objectuuid
+                          from recursive_grants
+                                   join rbacpermission perm on recursive_grants.descendantuuid = perm.uuid
+                                   join rbacobject obj on obj.uuid = perm.objectuuid
+                                   join count_check cc on cc.valid
+                          where obj.objectTable = '%1$s' -- 'SELECT' permission is included in all other permissions
             )
             select target.*
                 from %1$s as target
diff --git a/src/main/resources/db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.md b/src/main/resources/db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.md
index 270908a8..7fb81cd7 100644
--- a/src/main/resources/db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.md
+++ b/src/main/resources/db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.md
@@ -48,7 +48,7 @@ role:global:ADMIN -.-> role:debitorRel:OWNER
 role:debitorRel:OWNER -.-> role:debitorRel:ADMIN
 role:debitorRel:ADMIN -.-> role:debitorRel:AGENT
 role:debitorRel:AGENT -.-> role:debitorRel:TENANT
-role:debitorRel:AGENT ==> role:project:OWNER
+role:debitorRel:AGENT ==>|XX| role:project:OWNER
 role:project:OWNER ==> role:project:ADMIN
 role:project:ADMIN ==> role:project:AGENT
 role:project:AGENT ==> role:project:TENANT
diff --git a/src/main/resources/db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.sql b/src/main/resources/db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.sql
index e0e0a9b7..c6f3544d 100644
--- a/src/main/resources/db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.sql
+++ b/src/main/resources/db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.sql
@@ -49,7 +49,7 @@ begin
 
     perform createRoleWithGrants(
         hsBookingProjectOWNER(NEW),
-            incomingSuperRoles => array[hsOfficeRelationAGENT(newDebitorRel)]
+            incomingSuperRoles => array[hsOfficeRelationAGENT(newDebitorRel, unassumed())]
     );
 
     perform createRoleWithGrants(
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 019bb0a2..d06f9f9a 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
@@ -49,6 +49,7 @@ subgraph assignedToAsset["`**assignedToAsset**`"]
     subgraph assignedToAsset:roles[ ]
         style assignedToAsset:roles fill:#99bcdb,stroke:white
 
+        role:assignedToAsset:AGENT[[assignedToAsset:AGENT]]
         role:assignedToAsset:TENANT[[assignedToAsset:TENANT]]
     end
 end
@@ -97,6 +98,7 @@ role:asset:OWNER ==> role:asset:ADMIN
 role:bookingItem:AGENT ==> role:asset:ADMIN
 role:parentAsset:AGENT ==> role:asset:ADMIN
 role:asset:ADMIN ==> role:asset:AGENT
+role:assignedToAsset:AGENT ==> role:asset:AGENT
 role:asset:AGENT ==> role:assignedToAsset:TENANT
 role:asset:AGENT ==> role:alarmContact:REFERRER
 role:asset:AGENT ==> role:asset:TENANT
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 91afe2b6..5ec3e044 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
@@ -67,7 +67,9 @@ begin
 
     perform createRoleWithGrants(
         hsHostingAssetAGENT(NEW),
-            incomingSuperRoles => array[hsHostingAssetADMIN(NEW)],
+            incomingSuperRoles => array[
+            	hsHostingAssetADMIN(NEW),
+            	hsHostingAssetAGENT(newAssignedToAsset)],
             outgoingSubRoles => array[
             	hsHostingAssetTENANT(newAssignedToAsset),
             	hsOfficeContactREFERRER(newAlarmContact)]
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 9e8f3317..a74b6126 100644
--- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql
+++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql
@@ -23,6 +23,7 @@ declare
     managedServerUuid                   uuid;
     managedWebspaceUuid                 uuid;
     webUnixUserUuid                     uuid;
+    mboxUnixUserUuid                     uuid;
     domainSetupUuid                     uuid;
     domainMBoxSetupUuid                 uuid;
     mariaDbInstanceUuid                 uuid;
@@ -71,6 +72,7 @@ begin
     select uuid_generate_v4() into managedServerUuid;
     select uuid_generate_v4() into managedWebspaceUuid;
     select uuid_generate_v4() into webUnixUserUuid;
+    select uuid_generate_v4() into mboxUnixUserUuid;
     select uuid_generate_v4() into domainSetupUuid;
     select uuid_generate_v4() into domainMBoxSetupUuid;
     select uuid_generate_v4() into mariaDbInstanceUuid;
@@ -94,11 +96,12 @@ begin
        (uuid_generate_v4(),     null,                   'PGSQL_DATABASE',    pgSqlUserUuid,       pgSqlInstanceUuid,     defaultPrefix || '01_web',                           'some default Postgresql database','{ "encryption": "utf8", "collation": "utf8"}'::jsonb ),
        (uuid_generate_v4(),     null,                   'EMAIL_ALIAS',       managedWebspaceUuid, null,                  defaultPrefix || '01-web',                           'some E-Mail-Alias',            '{ "target": [ "office@example.org", "archive@example.com" ] }'::jsonb),
        (webUnixUserUuid,        null,                   'UNIX_USER',         managedWebspaceUuid, null,                  defaultPrefix || '01-web',                           'some UnixUser for Website',    '{ "SSD-soft-quota": "128", "SSD-hard-quota": "256", "HDD-soft-quota": "512", "HDD-hard-quota": "1024"}'::jsonb),
+       (mboxUnixUserUuid,       null,                   'UNIX_USER',         managedWebspaceUuid, null,                  defaultPrefix || '01-mbox',                          'some UnixUser for E-Mail',     '{ "SSD-soft-quota": "128", "SSD-hard-quota": "256", "HDD-soft-quota": "512", "HDD-hard-quota": "1024"}'::jsonb),
        (domainSetupUuid,        null,                   'DOMAIN_SETUP',      null,                null,                  defaultPrefix || '.example.org',                     'some Domain-Setup',            '{}'::jsonb),
        (uuid_generate_v4(),     null,                   'DOMAIN_DNS_SETUP',  domainSetupUuid,     null,                  defaultPrefix || '.example.org|DNS',                 'some Domain-DNS-Setup',        '{}'::jsonb),
        (uuid_generate_v4(),     null,                   'DOMAIN_HTTP_SETUP', domainSetupUuid,     webUnixUserUuid,       defaultPrefix || '.example.org|HTTP',                'some Domain-HTTP-Setup',       '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*"}'::jsonb),
-       (uuid_generate_v4(),     null,                   'DOMAIN_SMTP_SETUP', domainSetupUuid,     managedWebspaceUuid,   defaultPrefix || '.example.org|DNS',                 'some Domain-SMPT-Setup',       '{}'::jsonb),
-       (domainMBoxSetupUuid,    null,                   'DOMAIN_MBOX_SETUP', domainSetupUuid,     managedWebspaceUuid,   defaultPrefix || '.example.org|DNS',                 'some Domain-MBOX-Setup',       '{}'::jsonb),
+       (uuid_generate_v4(),     null,                   'DOMAIN_SMTP_SETUP', domainSetupUuid,     managedWebspaceUuid,   defaultPrefix || '.example.org|SMTP',                'some Domain-SMTP-Setup',       '{}'::jsonb),
+       (domainMBoxSetupUuid,    null,                   'DOMAIN_MBOX_SETUP', domainSetupUuid,     managedWebspaceUuid,   defaultPrefix || '.example.org|MBOX',                'some Domain-MBOX-Setup',       '{}'::jsonb),
        (uuid_generate_v4(),     null,                   'EMAIL_ADDRESS',     domainMBoxSetupUuid, null,                  'test@' || defaultPrefix || '.example.org',          'some E-Mail-Address',          '{}'::jsonb);
 end; $$;
 --//
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemControllerAcceptanceTest.java
index 71753976..b28e3e4e 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
@@ -287,7 +287,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
     class PatchBookingItem {
 
         @Test
-        void globalAdmin_canPatchAllUpdatablePropertiesOfBookingItem() {
+        void projectAgent_canPatchAllUpdatablePropertiesOfBookingItem() {
 
             final var givenBookingItem = givenSomeNewBookingItem("D-1000111 default project", MANAGED_WEBSPACE,
                     resource("HDD", 100), resource("SSD", 50), resource("Traffic", 250));
@@ -295,6 +295,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
             RestAssured // @formatter:off
                 .given()
                     .header("current-user", "superuser-alex@hostsharing.net")
+                    .header("assumed-roles", "hs_booking_project#D-1000111-D-1000111defaultproject:AGENT")
                     .contentType(ContentType.JSON)
                     .body("""
                         {
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 627eabc2..23e0307f 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemEntityUnitTest.java
@@ -53,7 +53,7 @@ class HsBookingItemEntityUnitTest {
     void toStringContainsAllPropertiesAndResourcesSortedByKey() {
         final var result = givenBookingItem.toString();
 
-        assertThat(result).isEqualToIgnoringWhitespace("HsBookingItemEntity(D-1234500:test project, CLOUD_SERVER, [2020-01-01,2031-01-01), some caption, { \"CPU\": 2, \"HDD-storage\": 2048, \"SSD-storage\": 512 })");
+        assertThat(result).isEqualToIgnoringWhitespace("HsBookingItemEntity(CLOUD_SERVER, some caption, D-1234500:test project, [2020-01-01,2031-01-01), { \"CPU\": 2, \"HDD-storage\": 2048, \"SSD-storage\": 512 })");
     }
 
     @Test
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java
index d0d58cfc..9c1c04d0 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/HsBookingItemRepositoryIntegrationTest.java
@@ -170,9 +170,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
             // then
             allTheseBookingItemsAreReturned(
                     result,
-                    "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 })",
-                    "HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPU: 2, RAM: 8, SSD: 500, Traffic: 500 })",
-                    "HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPU: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 })");
+                    "HsBookingItemEntity(MANAGED_SERVER, separate ManagedServer, D-1000212:D-1000212 default project, [2022-10-01,), { CPU: 2, RAM: 8, SSD: 500, Traffic: 500 })",
+                    "HsBookingItemEntity(MANAGED_WEBSPACE, separate ManagedWebspace, D-1000212:D-1000212 default project, [2022-10-01,), { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 })",
+                    "HsBookingItemEntity(PRIVATE_CLOUD, some PrivateCloud, D-1000212:D-1000212 default project, [2024-04-01,), { CPU: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 })");
              assertThat(result.stream().filter(bi -> bi.getRelatedHostingAsset()!=null).findAny())
                      .as("at least one relatedProject expected, but none found => fetching relatedProject does not work")
                      .isNotEmpty();
@@ -182,7 +182,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
         public void normalUser_canViewOnlyRelatedBookingItems() {
             // given:
             context("person-FirbySusan@example.com");
-            final var projectUuid = debitorRepo.findDebitorByDebitorNumber(1000111).stream()
+            final var debitor = debitorRepo.findDebitorByDebitorNumber(1000111);
+            context("person-FirbySusan@example.com", "hs_booking_project#D-1000111-D-1000111defaultproject:OWNER");
+            final var projectUuid = debitor.stream()
                     .map(d -> projectRepo.findAllByDebitorUuid(d.getUuid()))
                     .flatMap(List::stream)
                     .findAny().orElseThrow().getUuid();
@@ -193,9 +195,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
             // then:
             exactlyTheseBookingItemsAreReturned(
                     result,
-                    "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons : 0, Multi : 1, SSD : 100, Traffic : 50 })",
-                    "HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPU : 2, RAM : 8, SSD : 500, Traffic : 500 })",
-                    "HsBookingItemEntity(D-1000111:D-1000111 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPU : 10, HDD : 10000, RAM : 32, SSD : 4000, Traffic : 2000 })");
+                    "HsBookingItemEntity(MANAGED_SERVER, separate ManagedServer, D-1000111:D-1000111 default project, [2022-10-01,), { CPU : 2, RAM : 8, SSD : 500, Traffic : 500 })",
+                    "HsBookingItemEntity(MANAGED_WEBSPACE, separate ManagedWebspace, D-1000111:D-1000111 default project, [2022-10-01,), { Daemons : 0, Multi : 1, SSD : 100, Traffic : 50 })",
+                    "HsBookingItemEntity(PRIVATE_CLOUD, some PrivateCloud, D-1000111:D-1000111 default project, [2024-04-01,), { CPU : 10, HDD : 10000, RAM : 32, SSD : 4000, Traffic : 2000 })");
         }
     }
 
@@ -209,7 +211,7 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
 
             // when
             final var result = jpaAttempt.transacted(() -> {
-                context("superuser-alex@hostsharing.net");
+                context("superuser-alex@hostsharing.net", "hs_booking_project#D-1000111-D-1000111defaultproject:AGENT");
                 final var foundBookingItem = em.find(HsBookingItemEntity.class, givenBookingItemUuid);
                 foundBookingItem.getResources().put("CPU", 2);
                 foundBookingItem.getResources().remove("SSD-storage");
@@ -262,12 +264,12 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
         @Test
         public void nonGlobalAdmin_canNotDeleteTheirRelatedBookingItem() {
             // given
-            context("superuser-alex@hostsharing.net", null);
+            context("superuser-alex@hostsharing.net", "hs_booking_project#D-1000111-D-1000111defaultproject:AGENT");
             final var givenBookingItem = givenSomeTemporaryBookingItem("D-1000111 default project");
 
             // when
             final var result = jpaAttempt.transacted(() -> {
-                context("person-FirbySusan@example.com");
+                context("person-FirbySusan@example.com", "hs_booking_project#D-1000111-D-1000111defaultproject:AGENT");
                 assertThat(bookingItemRepo.findByUuid(givenBookingItem.getUuid())).isPresent();
 
                 bookingItemRepo.deleteByUuid(givenBookingItem.getUuid());
@@ -286,7 +288,7 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
         @Test
         public void deletingABookingItemAlsoDeletesRelatedRolesAndGrants() {
             // given
-            context("superuser-alex@hostsharing.net");
+            context("superuser-alex@hostsharing.net", "hs_booking_project#D-1000111-D-1000111defaultproject:AGENT");
             final var initialRoleNames = Array.from(distinctRoleNamesOf(rawRoleRepo.findAll()));
             final var initialGrantNames = Array.from(distinctGrantDisplaysOf(rawGrantRepo.findAll()));
             final var givenBookingItem = givenSomeTemporaryBookingItem("D-1000111 default project");
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectControllerAcceptanceTest.java
index 9a4c2391..94194b1f 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectControllerAcceptanceTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectControllerAcceptanceTest.java
@@ -163,7 +163,7 @@ class HsBookingProjectControllerAcceptanceTest extends ContextBasedTestWithClean
         }
 
         @Test
-        void debitorAgentUser_canGetRelatedBookingProject() {
+        void projectAgentUser_canGetRelatedBookingProject() {
             context.define("superuser-alex@hostsharing.net");
             final var givenBookingProjectUuid = bookingProjectRepo.findByCaption("D-1000313 default project").stream()
                     .findAny().orElseThrow().getUuid();
@@ -171,6 +171,7 @@ class HsBookingProjectControllerAcceptanceTest extends ContextBasedTestWithClean
             RestAssured // @formatter:off
                 .given()
                     .header("current-user", "person-TuckerJack@example.com")
+                    .header("assumed-roles", "hs_booking_project#D-1000313-D-1000313defaultproject:AGENT")
                     .port(port)
                 .when()
                     .get("http://localhost/api/hs/booking/projects/" + givenBookingProjectUuid)
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java
index e73bf942..8e3b7168 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/project/HsBookingProjectRepositoryIntegrationTest.java
@@ -125,7 +125,7 @@ class HsBookingProjectRepositoryIntegrationTest extends ContextBasedTestWithClea
                             "{ grant perm:hs_booking_project#D-1000111-somenewbookingproject:INSERT>hs_booking_item to role:hs_booking_project#D-1000111-somenewbookingproject:ADMIN by system and assume }",
 
                             // agent
-                            "{ grant role:hs_booking_project#D-1000111-somenewbookingproject:OWNER to role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:AGENT by system and assume }",
+                            "{ grant role:hs_booking_project#D-1000111-somenewbookingproject:OWNER to role:relation#FirstGmbH-with-DEBITOR-FirstGmbH:AGENT by system }",
                             "{ grant role:hs_booking_project#D-1000111-somenewbookingproject:TENANT to role:hs_booking_project#D-1000111-somenewbookingproject:AGENT by system and assume }",
 
                             // tenant
@@ -161,9 +161,10 @@ class HsBookingProjectRepositoryIntegrationTest extends ContextBasedTestWithClea
         }
 
         @Test
-        public void normalUser_canViewOnlyRelatedBookingProjects() {
+        public void packetAgent_canViewOnlyRelatedBookingProjects() {
+
             // given:
-            context("person-FirbySusan@example.com");
+            context("person-FirbySusan@example.com", "hs_booking_project#D-1000111-D-1000111defaultproject:AGENT");
             final var debitorUuid = debitorRepo.findByDebitorNumber(1000111).stream()
                     .findAny().orElseThrow().getUuid();
 
@@ -233,12 +234,11 @@ class HsBookingProjectRepositoryIntegrationTest extends ContextBasedTestWithClea
         @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");
+                context("person-FirbySusan@example.com", "hs_booking_project#D-1000111-sometempproject:AGENT");
                 assertThat(bookingProjectRepo.findByUuid(givenBookingProject.getUuid())).isPresent();
 
                 bookingProjectRepo.deleteByUuid(givenBookingProject.getUuid());
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 476b6bb0..0fcb35b4 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
@@ -324,10 +324,12 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
             assertThat(givenHostingAsset.getBookingItem().getResources().get("Multi"))
                     .as("precondition failed")
                     .isEqualTo(1);
+            final var preExistingUnixUserCount = assetRepo.findAllByCriteria(null, givenHostingAsset.getUuid(), UNIX_USER).size();
+            final var UNIX_USER_PER_MULTI_OPTION = 25;
 
             jpaAttempt.transacted(() -> {
                 context.define("superuser-alex@hostsharing.net");
-                for (int n = 0; n < 25; ++n) {
+                for (int n = 0; n < UNIX_USER_PER_MULTI_OPTION -preExistingUnixUserCount+1; ++n) {
                     toCleanup(assetRepo.save(
                             HsHostingAssetEntity.builder()
                                     .type(UNIX_USER)
@@ -413,7 +415,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
         }
 
         @Test
-        void debitorAgentUser_canGetRelatedAsset() {
+        void projectAgentUser_canGetRelatedAsset() {
             context.define("superuser-alex@hostsharing.net");
             final var givenAssetUuid = assetRepo.findByIdentifier("vm1013").stream()
                     .filter(bi -> bi.getBookingItem().getProject().getCaption().equals("D-1000313 default project"))
@@ -422,6 +424,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
             RestAssured // @formatter:off
                 .given()
                     .header("current-user", "person-TuckerJack@example.com")
+                    .header("assumed-roles", "hs_booking_project#D-1000313-D-1000313defaultproject:AGENT")
                     .port(port)
                 .when()
                     .get("http://localhost/api/hs/hosting/assets/" + givenAssetUuid)
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 ae733e54..682610de 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
@@ -28,6 +28,7 @@ import java.util.Map;
 import static java.util.Map.entry;
 import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;
 import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP;
+import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.EMAIL_ADDRESS;
 import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
 import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
 import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf;
@@ -98,7 +99,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
         @Test
         public void createsAndGrantsRoles() {
             // given
-            context("superuser-alex@hostsharing.net");
+            context("superuser-alex@hostsharing.net", "hs_booking_project#D-1000111-D-1000111defaultproject:AGENT");
             final var givenManagedServer = givenHostingAsset("D-1000111 default project", MANAGED_SERVER);
             final var newWebspaceBookingItem = newBookingItem(givenManagedServer.getBookingItem(), HsBookingItemType.MANAGED_WEBSPACE, "fir01");
             em.flush();
@@ -152,7 +153,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
                             "{ grant role:hs_booking_item#fir01:TENANT to role:hs_hosting_asset#fir00:TENANT by system and assume }",
                             "{ grant role:hs_hosting_asset#fir00:TENANT to role:hs_hosting_asset#fir00:AGENT by system and assume }",
                             "{ grant role:hs_hosting_asset#vm1011:TENANT to role:hs_hosting_asset#fir00:TENANT by system and assume }",
-                            "{ grant perm:hs_hosting_asset#fir00:SELECT to role:hs_hosting_asset#fir00:TENANT by system and assume }", // workaround
+                            "{ grant perm:hs_hosting_asset#fir00:SELECT to role:hs_hosting_asset#fir00:TENANT by system and assume }",
 
                             null));
         }
@@ -195,7 +196,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
     }
 
     @Nested
-    class FindByDebitorUuid {
+    class FindAssets {
 
         @Test
         public void globalAdmin_withoutAssumedRole_canViewArbitraryAssetsOfAllDebitors() {
@@ -214,9 +215,9 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
         }
 
         @Test
-        public void normalUser_canViewOnlyRelatedAsset() {
+        public void normalUser_canViewOnlyRelatedAssets() {
             // given:
-            context("person-FirbySusan@example.com");
+            context("person-FirbySusan@example.com", "hs_booking_project#D-1000111-D-1000111defaultproject:AGENT");
             final var projectUuid = projectRepo.findByCaption("D-1000111 default project").stream()
                     .findAny().orElseThrow().getUuid();
 
@@ -231,7 +232,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
         }
 
         @Test
-        public void normalUser_canFilterAssetsRelatedToParentAsset() {
+        public void managedServerAgent_canFindAssetsRelatedToManagedServer() {
             // given
             context("superuser-alex@hostsharing.net");
             final var parentAssetUuid = assetRepo.findByIdentifier("vm1012").stream()
@@ -249,6 +250,21 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
                     "HsHostingAssetEntity(MARIADB_INSTANCE, vm1012.MariaDB.default, some default MariaDB instance, MANAGED_SERVER:vm1012)",
                     "HsHostingAssetEntity(PGSQL_INSTANCE, vm1012.Postgresql.default, some default Postgresql instance, MANAGED_SERVER:vm1012)");
         }
+
+        @Test
+        public void managedServerAgent_canFindRelatedEmailAddresses() {
+            // given
+            context("superuser-alex@hostsharing.net");
+
+            // when
+            context("superuser-alex@hostsharing.net", "hs_hosting_asset#sec01:AGENT");
+            final var result = assetRepo.findAllByCriteria(null, null, EMAIL_ADDRESS);
+
+            // then
+            exactlyTheseAssetsAreReturned(
+                    result,
+                    "HsHostingAssetEntity(EMAIL_ADDRESS, test@sec.example.org, some E-Mail-Address, DOMAIN_MBOX_SETUP:sec.example.org|MBOX)");
+        }
     }
 
     @Nested
@@ -310,12 +326,12 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
         @Test
         public void relatedOwner_canDeleteTheirRelatedAsset() {
             // given
-            context("superuser-alex@hostsharing.net", null);
+            context("superuser-alex@hostsharing.net", "hs_booking_project#D-1000111-D-1000111defaultproject:AGENT");
             final var givenAsset = givenSomeTemporaryAsset("D-1000111 default project", "vm1000");
 
             // when
             final var result = jpaAttempt.transacted(() -> {
-                context("person-FirbySusan@example.com");
+                context("person-FirbySusan@example.com", "hs_booking_project#D-1000111-D-1000111defaultproject:AGENT");
                 assertThat(assetRepo.findByUuid(givenAsset.getUuid())).isPresent();
 
                 assetRepo.deleteByUuid(givenAsset.getUuid());
diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java
index 041424f4..410485eb 100644
--- a/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java
+++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/ImportHostingAssets.java
@@ -34,6 +34,7 @@ import org.springframework.test.annotation.DirtiesContext;
 import java.io.Reader;
 import java.net.IDN;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -183,9 +184,9 @@ public class ImportHostingAssets extends ImportOfficeData {
                 {
                    363=HsHostingAssetRealEntity(IPV4_NUMBER, 83.223.95.34),
                    381=HsHostingAssetRealEntity(IPV4_NUMBER, 83.223.95.52),
+                   401=HsHostingAssetRealEntity(IPV4_NUMBER, 83.223.95.72),
                    402=HsHostingAssetRealEntity(IPV4_NUMBER, 83.223.95.73),
-                   433=HsHostingAssetRealEntity(IPV4_NUMBER, 83.223.95.104),
-                   457=HsHostingAssetRealEntity(IPV4_NUMBER, 83.223.95.128)
+                   433=HsHostingAssetRealEntity(IPV4_NUMBER, 83.223.95.104)
                 }
                 """);
     }
@@ -239,13 +240,13 @@ public class ImportHostingAssets extends ImportOfficeData {
                 HsBookingItemType.MANAGED_SERVER,
                 HsBookingItemType.MANAGED_WEBSPACE)).isEqualToIgnoringWhitespace("""
                 {
-                   10630=HsBookingItemEntity(D-1000000:hsh default project, MANAGED_WEBSPACE, [2001-06-01,), BI hsh00),
-                   10968=HsBookingItemEntity(D-1015200:rar default project, MANAGED_SERVER, [2013-04-01,), BI vm1061),
-                   10978=HsBookingItemEntity(D-1000000:hsh default project, MANAGED_SERVER, [2013-04-01,), BI vm1050),
-                   11061=HsBookingItemEntity(D-1000300:mim default project, MANAGED_SERVER, [2013-08-19,), BI vm1068),
-                   11094=HsBookingItemEntity(D-1000300:mim default project, MANAGED_WEBSPACE, [2013-09-10,), BI lug00),
-                   11112=HsBookingItemEntity(D-1000300:mim default project, MANAGED_WEBSPACE, [2013-09-17,), BI mim00),
-                   23611=HsBookingItemEntity(D-1101800:wws default project, CLOUD_SERVER, [2022-08-10,), BI vm2097)
+                   10630=HsBookingItemEntity(MANAGED_WEBSPACE, BI hsh00, D-1000000:hsh default project, [2001-06-01,)),
+                   10968=HsBookingItemEntity(MANAGED_SERVER, BI vm1061, D-1015200:rar default project, [2013-04-01,)),
+                   10978=HsBookingItemEntity(MANAGED_SERVER, BI vm1050, D-1000000:hsh default project, [2013-04-01,)),
+                   11061=HsBookingItemEntity(MANAGED_SERVER, BI vm1068, D-1000300:mim default project, [2013-08-19,)),
+                   11094=HsBookingItemEntity(MANAGED_WEBSPACE, BI lug00, D-1000300:mim default project, [2013-09-10,)),
+                   11111=HsBookingItemEntity(MANAGED_WEBSPACE, BI xyz68, D-1000000:vm1068 Monitor, [2013-08-19,)),
+                   23611=HsBookingItemEntity(CLOUD_SERVER, BI vm2097, D-1101800:wws default project, [2022-08-10,))
                 }
                 """);
         assertThat(firstOfEach(9, packetAssets)).isEqualToIgnoringWhitespace("""
@@ -255,10 +256,10 @@ public class ImportHostingAssets extends ImportOfficeData {
                    10978=HsHostingAssetRealEntity(MANAGED_SERVER, vm1050, HA vm1050, D-1000000:hsh default project:BI vm1050),
                    11061=HsHostingAssetRealEntity(MANAGED_SERVER, vm1068, HA vm1068, D-1000300:mim default project:BI vm1068),
                    11094=HsHostingAssetRealEntity(MANAGED_WEBSPACE, lug00, HA lug00, MANAGED_SERVER:vm1068, D-1000300:mim default project:BI lug00),
+                   11111=HsHostingAssetRealEntity(MANAGED_WEBSPACE, xyz68, HA xyz68, MANAGED_SERVER:vm1068, D-1000000:vm1068 Monitor:BI xyz68),
                    11112=HsHostingAssetRealEntity(MANAGED_WEBSPACE, mim00, HA mim00, MANAGED_SERVER:vm1068, D-1000300:mim default project:BI mim00),
                    11447=HsHostingAssetRealEntity(MANAGED_SERVER, vm1093, HA vm1093, D-1000000:hsh default project:BI vm1093),
-                   19959=HsHostingAssetRealEntity(MANAGED_WEBSPACE, dph00, HA dph00, MANAGED_SERVER:vm1093, D-1101900:dph default project:BI dph00),
-                   23611=HsHostingAssetRealEntity(CLOUD_SERVER, vm2097, HA vm2097, D-1101800:wws default project:BI vm2097)
+                   19959=HsHostingAssetRealEntity(MANAGED_WEBSPACE, dph00, HA dph00, MANAGED_SERVER:vm1093, D-1101900:dph default project:BI dph00)
                 }
                 """);
     }
@@ -287,8 +288,8 @@ public class ImportHostingAssets extends ImportOfficeData {
                            10978=HsHostingAssetRealEntity(MANAGED_SERVER, vm1050, HA vm1050, D-1000000:hsh default project:BI vm1050),
                            11061=HsHostingAssetRealEntity(MANAGED_SERVER, vm1068, HA vm1068, D-1000300:mim default project:BI vm1068),
                            11094=HsHostingAssetRealEntity(MANAGED_WEBSPACE, lug00, HA lug00, MANAGED_SERVER:vm1068, D-1000300:mim default project:BI lug00),
-                           11112=HsHostingAssetRealEntity(MANAGED_WEBSPACE, mim00, HA mim00, MANAGED_SERVER:vm1068, D-1000300:mim default project:BI mim00),
-                           11447=HsHostingAssetRealEntity(MANAGED_SERVER, vm1093, HA vm1093, D-1000000:hsh default project:BI vm1093)
+                           11111=HsHostingAssetRealEntity(MANAGED_WEBSPACE, xyz68, HA xyz68, MANAGED_SERVER:vm1068, D-1000000:vm1068 Monitor:BI xyz68),
+                           11112=HsHostingAssetRealEntity(MANAGED_WEBSPACE, mim00, HA mim00, MANAGED_SERVER:vm1068, D-1000300:mim default project:BI mim00)
                         }
                         """);
         assertThat(firstOfEachType(
@@ -298,15 +299,16 @@ public class ImportHostingAssets extends ImportOfficeData {
                 HsBookingItemType.MANAGED_WEBSPACE))
                 .isEqualToIgnoringWhitespace("""
                         {
-                           10630=HsBookingItemEntity(D-1000000:hsh default project, MANAGED_WEBSPACE, [2001-06-01,), BI hsh00, {"HDD": 10, "Multi": 25, "SLA-Platform": "EXT24H", "SSD": 16, "Traffic": 50}),
-                           10968=HsBookingItemEntity(D-1015200:rar default project, MANAGED_SERVER, [2013-04-01,), BI vm1061, {"CPU": 6, "HDD": 250, "RAM": 14, "SLA-EMail": true, "SLA-Maria": true, "SLA-Office": true, "SLA-PgSQL": true, "SLA-Platform": "EXT4H", "SLA-Web": true, "SSD": 375, "Traffic": 250}),
-                           10978=HsBookingItemEntity(D-1000000:hsh default project, MANAGED_SERVER, [2013-04-01,), BI vm1050, {"CPU": 4, "HDD": 250, "RAM": 32, "SLA-EMail": true, "SLA-Maria": true, "SLA-Office": true, "SLA-PgSQL": true, "SLA-Platform": "EXT4H", "SLA-Web": true, "SSD": 150, "Traffic": 250}),
-                           11061=HsBookingItemEntity(D-1000300:mim default project, MANAGED_SERVER, [2013-08-19,), BI vm1068, {"CPU": 2, "HDD": 250, "RAM": 4, "SLA-EMail": true, "SLA-Maria": true, "SLA-Office": true, "SLA-PgSQL": true, "SLA-Platform": "EXT2H", "SLA-Web": true, "Traffic": 250}),
-                           11094=HsBookingItemEntity(D-1000300:mim default project, MANAGED_WEBSPACE, [2013-09-10,), BI lug00, {"Multi": 5, "SLA-Platform": "EXT24H", "SSD": 1, "Traffic": 10}),
-                           11112=HsBookingItemEntity(D-1000300:mim default project, MANAGED_WEBSPACE, [2013-09-17,), BI mim00, {"Multi": 5, "SLA-Platform": "EXT24H", "SSD": 3, "Traffic": 20}),
-                           11447=HsBookingItemEntity(D-1000000:hsh default project, MANAGED_SERVER, [2014-11-28,), BI vm1093, {"CPU": 6, "HDD": 500, "RAM": 16, "SLA-EMail": true, "SLA-Maria": true, "SLA-Office": true, "SLA-PgSQL": true, "SLA-Platform": "EXT4H", "SLA-Web": true, "SSD": 300, "Traffic": 250}),
-                           19959=HsBookingItemEntity(D-1101900:dph default project, MANAGED_WEBSPACE, [2021-06-02,), BI dph00, {"Multi": 1, "SLA-Platform": "EXT24H", "SSD": 25, "Traffic": 20}),
-                           23611=HsBookingItemEntity(D-1101800:wws default project, CLOUD_SERVER, [2022-08-10,), BI vm2097, {"CPU": 8, "RAM": 12, "SLA-Infrastructure": "EXT4H", "SSD": 25, "Traffic": 250})
+                           10630=HsBookingItemEntity(MANAGED_WEBSPACE, BI hsh00, D-1000000:hsh default project, [2001-06-01,), {"HDD": 10, "Multi": 25, "SLA-Platform": "EXT24H", "SSD": 16, "Traffic": 50}),
+                           10968=HsBookingItemEntity(MANAGED_SERVER, BI vm1061, D-1015200:rar default project, [2013-04-01,), {"CPU": 6, "HDD": 250, "RAM": 14, "SLA-EMail": true, "SLA-Maria": true, "SLA-Office": true, "SLA-PgSQL": true, "SLA-Platform": "EXT4H", "SLA-Web": true, "SSD": 375, "Traffic": 250}),
+                           10978=HsBookingItemEntity(MANAGED_SERVER, BI vm1050, D-1000000:hsh default project, [2013-04-01,), {"CPU": 4, "HDD": 250, "RAM": 32, "SLA-EMail": true, "SLA-Maria": true, "SLA-Office": true, "SLA-PgSQL": true, "SLA-Platform": "EXT4H", "SLA-Web": true, "SSD": 150, "Traffic": 250}),
+                           11061=HsBookingItemEntity(MANAGED_SERVER, BI vm1068, D-1000300:mim default project, [2013-08-19,), {"CPU": 2, "HDD": 250, "RAM": 4, "SLA-EMail": true, "SLA-Maria": true, "SLA-Office": true, "SLA-PgSQL": true, "SLA-Platform": "EXT2H", "SLA-Web": true, "Traffic": 250}),
+                           11094=HsBookingItemEntity(MANAGED_WEBSPACE, BI lug00, D-1000300:mim default project, [2013-09-10,), {"Multi": 5, "SLA-Platform": "EXT24H", "SSD": 1, "Traffic": 10}),
+                           11111=HsBookingItemEntity(MANAGED_WEBSPACE, BI xyz68, D-1000000:vm1068 Monitor, [2013-08-19,), {"SSD": 3}),
+                           11112=HsBookingItemEntity(MANAGED_WEBSPACE, BI mim00, D-1000300:mim default project, [2013-09-17,), {"Multi": 5, "SLA-Platform": "EXT24H", "SSD": 3, "Traffic": 20}),
+                           11447=HsBookingItemEntity(MANAGED_SERVER, BI vm1093, D-1000000:hsh default project, [2014-11-28,), {"CPU": 6, "HDD": 500, "RAM": 16, "SLA-EMail": true, "SLA-Maria": true, "SLA-Office": true, "SLA-PgSQL": true, "SLA-Platform": "EXT4H", "SLA-Web": true, "SSD": 300, "Traffic": 250}),
+                           19959=HsBookingItemEntity(MANAGED_WEBSPACE, BI dph00, D-1101900:dph default project, [2021-06-02,), {"Multi": 1, "SLA-Platform": "EXT24H", "SSD": 25, "Traffic": 20}),
+                           23611=HsBookingItemEntity(CLOUD_SERVER, BI vm2097, D-1101800:wws default project, [2022-08-10,), {"CPU": 8, "RAM": 12, "SLA-Infrastructure": "EXT4H", "SSD": 25, "Traffic": 250})
                         }
                         """);
     }
@@ -335,6 +337,7 @@ public class ImportHostingAssets extends ImportOfficeData {
                    5811=HsHostingAssetRealEntity(UNIX_USER, lug00-ola.a, LUG OLA - POP a, MANAGED_WEBSPACE:lug00, {"SSD hard quota": 0, "SSD soft quota": 0, "locked": false, "shell": "/usr/bin/passwd", "userid": 102094}),
                    5813=HsHostingAssetRealEntity(UNIX_USER, lug00-ola.b, LUG OLA - POP b, MANAGED_WEBSPACE:lug00, {"SSD hard quota": 0, "SSD soft quota": 0, "locked": false, "shell": "/usr/bin/passwd", "userid": 102095}),
                    5835=HsHostingAssetRealEntity(UNIX_USER, lug00-test, Test, MANAGED_WEBSPACE:lug00, {"SSD hard quota": 1024, "SSD soft quota": 1024, "locked": false, "shell": "/usr/bin/passwd", "userid": 102106}),
+                   5961=HsHostingAssetRealEntity(UNIX_USER, xyz68, Monitoring h68, MANAGED_WEBSPACE:xyz68, {"SSD hard quota": 0, "SSD soft quota": 0, "locked": false, "shell": "/bin/bash", "userid": 102141}),
                    5964=HsHostingAssetRealEntity(UNIX_USER, mim00, Michael Mellis, MANAGED_WEBSPACE:mim00, {"SSD hard quota": 0, "SSD soft quota": 0, "locked": false, "shell": "/bin/bash", "userid": 102147}),
                    5966=HsHostingAssetRealEntity(UNIX_USER, mim00-1981, Jahrgangstreffen 1981, MANAGED_WEBSPACE:mim00, {"SSD hard quota": 256, "SSD soft quota": 128, "locked": false, "shell": "/bin/bash", "userid": 102148}),
                    5990=HsHostingAssetRealEntity(UNIX_USER, mim00-mail, Mailbox, MANAGED_WEBSPACE:mim00, {"SSD hard quota": 0, "SSD soft quota": 0, "locked": false, "shell": "/bin/bash", "userid": 102160}),
@@ -880,6 +883,7 @@ public class ImportHostingAssets extends ImportOfficeData {
                    5811=HsHostingAssetRealEntity(UNIX_USER, lug00-ola.a, LUG OLA - POP a, MANAGED_WEBSPACE:lug00, {"SSD hard quota": 0, "SSD soft quota": 0, "locked": false, "password": null, "shell": "/usr/bin/passwd", "userid": 102094}),
                    5813=HsHostingAssetRealEntity(UNIX_USER, lug00-ola.b, LUG OLA - POP b, MANAGED_WEBSPACE:lug00, {"SSD hard quota": 0, "SSD soft quota": 0, "locked": false, "password": null, "shell": "/usr/bin/passwd", "userid": 102095}),
                    5835=HsHostingAssetRealEntity(UNIX_USER, lug00-test, Test, MANAGED_WEBSPACE:lug00, {"SSD hard quota": 1024, "SSD soft quota": 1024, "locked": false, "password": null, "shell": "/usr/bin/passwd", "userid": 102106}),
+                   5961=HsHostingAssetRealEntity(UNIX_USER, xyz68, Monitoring h68, MANAGED_WEBSPACE:xyz68, {"SSD hard quota": 0, "SSD soft quota": 0, "locked": false, "password": null, "shell": "/bin/bash", "userid": 102141}),
                    5964=HsHostingAssetRealEntity(UNIX_USER, mim00, Michael Mellis, MANAGED_WEBSPACE:mim00, {"SSD hard quota": 0, "SSD soft quota": 0, "locked": false, "password": null, "shell": "/bin/bash", "userid": 102147}),
                    5966=HsHostingAssetRealEntity(UNIX_USER, mim00-1981, Jahrgangstreffen 1981, MANAGED_WEBSPACE:mim00, {"SSD hard quota": 256, "SSD soft quota": 128, "locked": false, "password": null, "shell": "/bin/bash", "userid": 102148}),
                    5990=HsHostingAssetRealEntity(UNIX_USER, mim00-mail, Mailbox, MANAGED_WEBSPACE:mim00, {"SSD hard quota": 0, "SSD soft quota": 0, "locked": false, "password": null, "shell": "/bin/bash", "userid": 102160}),
@@ -909,8 +913,8 @@ public class ImportHostingAssets extends ImportOfficeData {
 
         verifyActuallyPersistedHostingAssetCount(CLOUD_SERVER, 1, 50);
         verifyActuallyPersistedHostingAssetCount(MANAGED_SERVER, 4, 100);
-        verifyActuallyPersistedHostingAssetCount(MANAGED_WEBSPACE, 4, 100);
-        verifyActuallyPersistedHostingAssetCount(UNIX_USER, 14, 100);
+        verifyActuallyPersistedHostingAssetCount(MANAGED_WEBSPACE, 5, 100);
+        verifyActuallyPersistedHostingAssetCount(UNIX_USER, 15, 100);
         verifyActuallyPersistedHostingAssetCount(EMAIL_ALIAS, 9, 1400);
         verifyActuallyPersistedHostingAssetCount(PGSQL_DATABASE, 8, 100);
         verifyActuallyPersistedHostingAssetCount(MARIADB_DATABASE, 8, 100);
@@ -918,6 +922,19 @@ public class ImportHostingAssets extends ImportOfficeData {
         verifyActuallyPersistedHostingAssetCount(EMAIL_ADDRESS, 71, 30000);
     }
 
+    @Test
+    @Order(19930)
+    void verifyProjectAgentsCanViewEmailAddresses() {
+        assumeThatWeAreImportingControlledTestData();
+
+        final var haCount = jpaAttempt.transacted(() -> {
+                    context(rbacSuperuser, "hs_booking_project#D-1000300-mimdefaultproject:AGENT");
+                    return (Integer) em.createNativeQuery("select count(*) from hs_hosting_asset_rv where type='EMAIL_ADDRESS'", Integer.class)
+                            .getSingleResult();
+                }).assertSuccessful().returnedValue();
+        assertThat(haCount).isEqualTo(68);
+    }
+
     // ============================================================================================
 
     @Test
@@ -1095,7 +1112,20 @@ public class ImportHostingAssets extends ImportOfficeData {
                         final var managedWebspace = pac(packet_id);
                         final var parentAsset = hive(hive_id).serverRef.get();
                         managedWebspace.setParentAsset(parentAsset);
-                        managedWebspace.getBookingItem().setParentItem(parentAsset.getBookingItem());
+
+                        if (parentAsset.getRelatedProject() != managedWebspace.getRelatedProject()
+                                && managedWebspace.getRelatedProject().getDebitor().getDebitorNumber() == 10000_00 ) {
+                            assertThat(managedWebspace.getIdentifier()).startsWith("xyz");
+                            final var hshDebitor = managedWebspace.getBookingItem().getProject().getDebitor();
+                            final var newProject = HsBookingProjectEntity.builder()
+                                    .debitor(hshDebitor)
+                                    .caption(parentAsset.getIdentifier() + " Monitor")
+                                    .build();
+                            bookingProjects.put(Collections.max(bookingProjects.keySet())+1, newProject);
+                            managedWebspace.getBookingItem().setProject(newProject);
+                        } else {
+                            managedWebspace.getBookingItem().setParentItem(parentAsset.getBookingItem());
+                        }
                     }
                 });
     }
diff --git a/src/test/resources/migration/hosting/inet_addr.csv b/src/test/resources/migration/hosting/inet_addr.csv
index 15bab1fb..bee797c4 100644
--- a/src/test/resources/migration/hosting/inet_addr.csv
+++ b/src/test/resources/migration/hosting/inet_addr.csv
@@ -1,6 +1,7 @@
 inet_addr_id;inet_addr;description
 363;83.223.95.34;
 381;83.223.95.52;
+401;83.223.95.72;
 402;83.223.95.73;
 433;83.223.95.104;
 457;83.223.95.128;
diff --git a/src/test/resources/migration/hosting/packet.csv b/src/test/resources/migration/hosting/packet.csv
index 63637444..6e27b41b 100644
--- a/src/test/resources/migration/hosting/packet.csv
+++ b/src/test/resources/migration/hosting/packet.csv
@@ -4,6 +4,7 @@ packet_id;basepacket_code;packet_name;bp_id;hive_id;created;cancelled;cur_inet_a
 10978;SRV/MGD;vm1050;213;1014;2013-04-01;;433;;1
 11061;SRV/MGD;vm1068;100;1037;2013-08-19;;381;;f
 11094;PAC/WEB;lug00;100;1037;2013-09-10;;1168;;1
+11111;PAC/WEB;xyz68;213;1037;2013-08-19;;401;;1
 11112;PAC/WEB;mim00;100;1037;2013-09-17;;402;;1
 11447;SRV/MGD;vm1093;213;1163;2014-11-28;;457;;t
 19959;PAC/WEB;dph00;542;1163;2021-06-02;;574;;0
diff --git a/src/test/resources/migration/hosting/packet_component.csv b/src/test/resources/migration/hosting/packet_component.csv
index ce35034f..5dee11ad 100644
--- a/src/test/resources/migration/hosting/packet_component.csv
+++ b/src/test/resources/migration/hosting/packet_component.csv
@@ -7,6 +7,7 @@ packet_component_id;packet_id;quantity;basecomponent_code;created;cancelled
 46121;11112;20;TRAFFIC;2017-03-27;
 46122;11112;5;MULTI;2017-03-27;
 46123;11112;3072;QUOTA;2017-03-27;
+46124;11111;3072;QUOTA;2017-03-27;
 143133;11094;1;SLABASIC;2017-09-01;
 143483;11112;1;SLABASIC;2017-09-01;
 757383;11112;0;SLAEXT24H;;
diff --git a/src/test/resources/migration/hosting/unixuser.csv b/src/test/resources/migration/hosting/unixuser.csv
index cd044e0a..739899d2 100644
--- a/src/test/resources/migration/hosting/unixuser.csv
+++ b/src/test/resources/migration/hosting/unixuser.csv
@@ -9,6 +9,7 @@ unixuser_id;name;comment;shell;homedir;locked;packet_id;userid;quota_softlimit;q
 5835;lug00-test;Test;/usr/bin/passwd;/home/pacs/lug00/users/test;0;11094;102106;2000000;4000000;20;0
 
 6705;hsh00-mim;Michael Mellis;/bin/false;/home/pacs/hsh00/users/mi;0;10630;10003;0;0;0;0
+5961;xyz68;Monitoring h68;/bin/bash;/home/pacs/xyz68;0;11111;102141;0;0;0;0
 5964;mim00;Michael Mellis;/bin/bash;/home/pacs/mim00;0;11112;102147;0;0;0;0
 5966;mim00-1981;Jahrgangstreffen 1981;/bin/bash;/home/pacs/mim00/users/1981;0;11112;102148;128;256;0;0
 5990;mim00-mail;Mailbox;/bin/bash;/home/pacs/mim00/users/mail;0;11112;102160;0;0;0;0