Bite #30H — Tenant Administrator Cardinality - RodrigoMattosoSilveira/EnterpriseRemoteSystems GitHub Wiki
Enterprise Remote Systems — Bite 30H Manual Promotion Tests
Promotion candidate
- Bite: Bite #30H — Tenant Administrator Cardinality
- Branch:
736-bite-30h---tenant-administrator-cardinality - Committed SHA supplied for promotion:
d02e1f65e495b458a77553fabafb4f2c61d183be - Required cardinality:
- Tenant → active
TENANT_ADMINRole Grants: 0..2 - Global Person → Tenants concurrently administered: 0..1
- When a Tenant has two active
TENANT_ADMINRole Grants, the grants must belong to two distinct global Persons. - Deactivating an Actor does not free a Tenant Administrator slot. Only explicit Role Grant revocation frees the slot.
- Tenant → active
Source-verification note: the ZIP for SHA
d02e1f65e495b458a77553fabafb4f2c61d183bewas not exposed to the assistant runtime when this suite was generated. The suite was derived from the immediately preceding Bite 30H source plus the final E2E correction that led to the reported all-green local gates. Before promotion, confirm the attached/current branch contains the same000062_tenant_administrator_cardinalitymigration and the final E2E cleanup changes.
1. Promotion-test philosophy
These are functional manual tests first. Perform Tests 01–10 through the supported product UI.
Do not use SQL or DevTools to substitute for information that should be visible in the UI. If the UI cannot show the cardinality state, candidate eligibility, current assignment count, inactive-Actor state, or grant blocker described below, treat that as an observability/UX defect.
A separate technical promotion gate appears after the UI tests. SQL there is read-only except for creation/removal of a disposable temporary migration database.
2. Preconditions
Before beginning:
- All Bite 30H software changes are committed.
-
git status --shortis empty. -
git rev-parse HEADprints exactly:
d02e1f65e495b458a77553fabafb4f2c61d183be
-
make local-checkpasses. - Full Playwright passes.
- Backend is stopped before fixture reset.
Prepare deterministic data
Use the supplied Bite 30H wrapper from the project root:
python3 ~/Downloads/seed-bite30h-manual-testdata.py \
--repo-root . \
--db-path backend/data/app.db
The wrapper invokes the branch's canonical:
scripts/seed-bite30g-final-manual-testdata.py --reset-existing-batch
and verifies the Bite 30H database guards and baseline administrator assignments.
Then restart ERS:
make local-backend
In another terminal:
make local-frontend
Use a fresh browser session after the reset.
3. Deterministic identities
Application Administrator
For the standard local environment initialized by scripts/init-dev-env.sh:
- Login:
[email protected] - Password:
admin123
If your backend/.env overrides DEV_ADMIN_EMAIL or DEV_ADMIN_PASSWORD, use those configured values instead.
Common Bite 30G/30H fixture password
Manual-30C-Password!
Tenants
Tenant A
- Name: Byte 28A Manual Test
- Deterministic fixture ID when the fixture creates it:
manual30g-tenant-a
Tenant B
- Name: Default Tenant
- ID:
default
Starting Tenant Administrators
Operator D — Tenant A Administrator
- Login:
[email protected] - Actor display: 30G Operator D · Tenant A
- Actor key:
manual30g-actor-key-operator-tenant-a
Operator E — Tenant B Administrator
- Login:
[email protected] - Actor display: 30G Operator E · Tenant B
- Actor key:
manual30g-actor-key-tenant-b-admin
Cardinality candidates
Identity A — same global Person in two Tenants
- Login:
[email protected] - Global Person:
manual30g-global-person-a - Tenant A Actor display: 30G Identity A · Tenant A
- Tenant A Actor key:
manual30g-actor-key-a-tenant-a - Tenant B Actor display: 30G Identity A · Tenant B
- Tenant B Actor key:
manual30g-actor-key-a-tenant-b
Identity A is the critical fixture for proving that two Tenant Actors cannot bypass the Person 0..1-across-Tenants rule.
Identity B — distinct Person in Tenant A
- Login:
[email protected] - Global Person:
manual30g-global-person-b - Actor display: 30G Identity B · Tenant owes
- Actor key:
manual30g-actor-key-b-tenant-a
Identity C — additional distinct Person in Tenant A
- Login:
[email protected] - Global Person:
manual30g-global-person-c - Actor display: 30G Identity C · Collaborator owes
- Actor key:
manual30g-actor-key-c-tenant-a
4. State discipline
Perform the tests in order. Several tests deliberately change Role Grants and then restore them.
Expected initial state:
| Tenant | Active TENANT_ADMIN assignments |
Active administrator Actors |
|---|---|---|
| Tenant A | 1/2 — Operator D | 1 |
| Tenant B | 1/2 — Operator E | 1 |
Expected final restored state after Test 10:
| Tenant | Active TENANT_ADMIN assignments |
Active administrator Actors |
|---|---|---|
| Tenant A | 1/2 — Operator D | 1 |
| Tenant B | 1/2 — Operator E | 1 |
If your state diverges unexpectedly, stop rather than compensating with SQL.
[✔︎] Test 01 — Baseline Tenant Administrator cardinality is visible
Purpose
Verify the product exposes the 30H assignment capacity separately from current Actor availability.
Steps
- Sign in as the Application Administrator.
- Open Tenants.
- Open Byte 28A Manual Test.
- In Operational status, verify the Tenant is
ACTIVE_READY. - [✔︎] Verify Tenant administrators displays:
1 of 2 assignments(1 active Actor)
- In the Tenant administrators section, verify 30G Operator D · Tenant A is listed.
- [✔︎] Verify the page explains:
- a Tenant may have up to two active
TENANT_ADMINassignments; - the assignments must belong to distinct Persons;
- a Person may administer only one Tenant at a time;
- Actor deactivation does not release an assignment.
- a Tenant may have up to two active
- [✔︎] Verify the blue information message says one of two slots is occupied and a second distinct Person may be assigned for redundancy.
- [✔︎] Verify the selector reads Select an eligible active actor and Assign Admin is disabled until a candidate is selected.
- Return to Tenants and open Default Tenant.
- [✔︎] Verify
1 of 2 assignments,(1 active Actor), and 30G Operator E · Tenant B.
Pass criteria
- Tenant A visibly reports
1 of 2 assignmentsand one active administrator Actor. - Tenant B visibly reports
1 of 2 assignmentsand one active administrator Actor. - Assignment capacity and active-Actor count are presented as different concepts.
- The 30H rules are understandable without SQL or DevTools.
[x] Test 02 — A second distinct Person can occupy the backup slot
Purpose
Prove the Tenant upper bound is two, not one, and establish the cross-Tenant Person test condition.
Steps
- As Application Administrator, open Default Tenant.
- In Tenant administrators, open the eligible-Actor selector.
- Select 30G Identity A · Tenant B.
- Click Assign Admin.
- Verify the success message confirms assignment.
- Verify the page now reports:
2 of 2 assignments(2 active Actors)
- Verify both are listed:
- 30G Operator E · Tenant B
- 30G Identity A · Tenant B
- Verify the page says Both Tenant Administrator slots are occupied.
- Verify the selector is disabled and its placeholder says Maximum of two administrators assigned.
Pass criteria
- A second distinct global Person can be assigned.
- The Tenant reaches exactly 2/2.
- A third assignment cannot be initiated from Tenant Management.
Do not revoke Identity A yet. Test 03 depends on this state.
[x] Test 03 — The same global Person cannot administer two Tenants
Purpose
Prove that separate Tenant Actors do not let one global Person bridge privileged Tenant data.
Steps — Tenant Management path
- With Identity A still a Tenant Administrator in Default Tenant, open Byte 28A Manual Test.
- Verify Tenant A still reports
1 of 2 assignments. - Locate Unavailable administrator candidates.
- Find 30G Identity A · Tenant A.
- Verify it is not present in the eligible selector.
- Verify its ineligibility reason states that this Person already administers tenant
default.
Steps — Application Authorization path
- Open Authorization.
- Find the Actor card for:
manual30g-actor-key-a-tenant-a
- Verify its binding shows an Authentication Account and an ACTIVE Person–Tenant Membership for Tenant A.
- Choose role
TENANT_ADMIN. - Verify Grant tenant resolves to Tenant A's immutable Tenant ID, not
defaultand not*. - Verify Grant Role is disabled.
- Verify the page displays a blocker equivalent to:
TENANT_ADMIN grant unavailable: This Person already administers tenant default; a Person may administer only one Tenant at a time.
- Change the role selector to a normal compatible tenant operator Role such as
EXPENSE_OPERATOR. - Verify the cross-Tenant Tenant Administrator blocker disappears. Do not grant the operator Role.
Cleanup
- Return to Default Tenant.
- Click Revoke for 30G Identity A · Tenant B.
- Verify Default Tenant returns to:
1 of 2 assignments(1 active Actor)- only 30G Operator E · Tenant B assigned.
Pass criteria
- The same global Person cannot receive
TENANT_ADMINin Tenant A while administering Tenant B. - The restriction follows the global Person across different Tenant Actor records.
- The restriction is specific to
TENANT_ADMIN; normal compatible delegated roles are not incorrectly blocked by the Person 0..1 rule. - Revocation releases Identity A from the cross-Tenant restriction.
Test 04 — Tenant A can reach exactly two administrators
Purpose
Establish a full 2/2 Tenant for maximum-capacity and lifecycle tests.
Steps
- Open Byte 28A Manual Test.
- Select 30G Identity A · Tenant A from Select an eligible active actor.
- Click Assign Admin.
- Verify the page reports:
2 of 2 assignments(2 active Actors)
- Verify both are listed:
- 30G Operator D · Tenant A
- 30G Identity A · Tenant A
- Verify Both Tenant Administrator slots are occupied.
- Verify the assignment selector is disabled with Maximum of two administrators assigned.
Pass criteria
- Tenant A accepts a second distinct Person.
- The product presents 2/2 as a valid redundancy state, not an error state.
Leave Tenant A at 2/2 for Tests 05–06.
Test 05 — A third Tenant Administrator is blocked in both administration surfaces
Purpose
Prove the Tenant upper bound is enforced before an invalid submission.
Tenant Management path
- Stay on Byte 28A Manual Test.
- Verify the assignment selector is disabled at 2/2.
- Verify neither Identity B nor Identity C can be selected for a third assignment.
Application Authorization path
- Open Authorization.
- Find Actor:
manual30g-actor-key-b-tenant-a
- Select
TENANT_ADMIN. - Verify Grant Role is disabled.
- Verify the blocker says:
Tenant already has the maximum of two active Tenant Administrators.
- Repeat with Actor:
manual30g-actor-key-c-tenant-a
- Verify the same maximum-of-two blocker.
Pass criteria
- Tenant Management prevents the third assignment.
- Application Authorization independently prevents the third assignment.
- No transient third
TENANT_ADMINgrant appears.
Test 06 — Deactivating an administrator Actor does not release its slot
Purpose
Prove cardinality belongs to the active Role Grant rather than Actor availability.
Steps
- In Authorization, find:
manual30g-actor-key-a-tenant-a
- Confirm its active
TENANT_ADMINRole Grant for Tenant A is visible. - Click Deactivate for the Actor.
- Verify the Actor card now says Inactive while the
TENANT_ADMINRole Grant remains listed. - Open Byte 28A Manual Test.
- Verify the page still reports:
2 of 2 assignments(1 active Actor)
- Verify 30G Identity A · Tenant A remains in the assigned administrator list and is labeled Inactive actor.
- Verify Both Tenant Administrator slots are occupied still appears.
- Verify the assignment selector remains disabled.
- Open Authorization, find Identity B's Tenant A Actor, choose
TENANT_ADMIN, and verify the maximum-of-two blocker still applies.
Pass criteria
- Actor deactivation reduces active administrator Actor count from 2 to 1.
- Assignment count remains 2/2.
- The inactive Actor's grant still consumes a slot.
- A third administrator remains prohibited.
Leave Identity A's Actor inactive and its Role Grant active for Test 07.
Test 07 — Explicit Role Grant revocation releases the slot
Purpose
Prove only explicit authorization revocation releases Tenant Administrator capacity.
Steps
- Open Byte 28A Manual Test.
- Click Revoke for 30G Identity A · Tenant A.
- Verify the success message says the Tenant Administrator assignment was revoked.
- Verify Tenant A now reports:
1 of 2 assignments(1 active Actor)
- Verify the eligible selector is enabled again.
- Under Unavailable administrator candidates, verify the now-unassigned Identity A Actor is unavailable because it is inactive, with a reason equivalent to:
Inactive actors cannot be assigned as Tenant Administrators
- Open Authorization.
- Find
manual30g-actor-key-a-tenant-a. - manual30g-actor-key-a-tenant-aVerify the
TENANT_ADMINgrant is gone/inactive. - Click Activate to restore Identity A's Actor for later regression use.
Pass criteria
- Revocation, not Actor deactivation, releases the second slot.
- Tenant A returns to 1/2.
- An inactive unassigned Actor is not eligible for a new Tenant Administrator assignment.
- Reactivation alone does not recreate the revoked
TENANT_ADMINRole Grant.
Test 08 — Safe administrator handoff without a coverage gap
Purpose
Verify the reason for allowing two slots: a replacement can be verified before the departing administrator is revoked.
Establish overlap
- Open Byte 28A Manual Test as Application Administrator.
- Select 30G Identity B · Tenant owes.
- Click Assign Admin.
- Verify Tenant A reports
2 of 2 assignmentsand two active Actors:- Operator D
- Identity B
Verify the replacement before removing the original
- Sign out.
- Sign in as:
[email protected]
with password:
Manual-30C-Password!
- Verify the session is in Tenant A / Byte 28A Manual Test.
- Verify Tenant Administrator capabilities are visible in navigation, including Authorization and Reference data.
- Verify application-only control-plane links Tenants and Authentication are not exposed merely because Identity B is a Tenant Administrator.
- Open Authorization and verify the page loads for Tenant A rather than returning Forbidden.
Complete the handoff
- Sign out Identity B and sign back in as Application Administrator.
- Open Byte 28A Manual Test.
- Revoke 30G Operator D · Tenant A.
- Verify the Tenant remains
ACTIVE_READY. - Verify it reports:
1 of 2 assignments(1 active Actor)- Identity B is the remaining administrator.
- Select 30G Operator D · Tenant A and assign it again. 17.[x] Verify 2/2 is restored.
- Revoke 30G Identity B · Tenant owes.
- Verify the initial Tenant A baseline is restored:
1 of 2 assignments(1 active Actor)- Operator D is the sole Tenant Administrator.
Pass criteria
- A second administrator can be assigned before removing the first.
- The replacement can actually authenticate and exercise Tenant Administrator navigation/authorization.
- Removing the original after verification does not create an administrative coverage gap.
- The baseline can be restored without recreating Actors or Authentication Accounts.
Test 09 — Zero Tenant Administrators is a valid transitional state but not operationally ready
Purpose
Verify the lower bound is zero while the product clearly warns about loss of administrative coverage.
Steps
- As Application Administrator, open Byte 28A Manual Test.
- Confirm Operator D is currently the only Tenant Administrator.
- Click Revoke for Operator D.
- Verify the Tenant remains an existing active Tenant; it is not deleted or automatically deactivated.
- Verify Operational status becomes
ACTIVE_NO_TENANT_ADMIN. - Verify the administrator summary reports:
0 of 2 assignments(0 active Actors)
- Verify the warning says:
No Tenant Administrator is assigned. Assign at least one for operational readiness.
- Verify the eligible Actor selector remains available.
- Select 30G Operator D · Tenant A.
- Click Assign Admin.
- Verify the Tenant returns to
ACTIVE_READYand1 of 2 assignments/(1 active Actor).
Pass criteria
- Zero active assignments is accepted as a transitional state.
- The UI clearly reports lack of administrative coverage.
- Restoring one administrator returns the Tenant to ready state.
Test 10 — Ordinary multi-Tenant Person access is not prohibited by the administrator cardinality rule
Purpose
Confirm Bite 30H restricts privileged TENANT_ADMIN authority, not ordinary Person membership or self-service across Tenants.
Steps
- Confirm Identity A has no
TENANT_ADMINRole Grant in either Tenant after prior cleanup. - Sign out of the Application Administrator account.
- Sign in as:
[email protected]
using:
Manual-30C-Password!
- Verify Identity A can select/access Byte 28A Manual Test.
- Verify Identity A can access its Person/self-service information for Tenant A.
- Switch to Default Tenant.
- Verify the tenant switch succeeds.
- Verify Identity A can access its Person/self-service information for Tenant B.
- Verify the account is not blocked merely because the same global Person has memberships/Actors in two Tenants.
- Verify Tenant Administrator-only capabilities are not present unless a
TENANT_ADMINgrant actually exists. - Sign out.
- Sign back in as Application Administrator.
- Reopen Tenant A and Tenant B and verify final baseline:
| Tenant | Expected final state |
|---|---|
| Tenant A | Operator D only, 1/2, 1 active Actor, ACTIVE_READY |
| Tenant B | Operator E only, 1/2, 1 active Actor, ACTIVE_READY |
Pass criteria
- The Person 0..1-across-Tenants rule applies only to active
TENANT_ADMINauthority. - Ordinary multi-Tenant membership and self-service remain functional.
- Both fixture Tenants are restored to their original administrator state.
5. Manual-suite promotion decision
The UI manual suite passes only if all of the following are true:
- Tenant A and Tenant B initially expose 1/2 administrator capacity.
- A second distinct Person can be assigned.
- A third administrator is blocked.
- The same global Person cannot administer Tenant A and Tenant B concurrently through separate Actors.
- The cross-Tenant restriction does not incorrectly block normal non-
TENANT_ADMINdelegated roles. - Actor deactivation does not release a Tenant Administrator slot.
- Explicit Role Grant revocation does release the slot.
- An inactive unassigned Actor is not eligible for new Tenant Administrator assignment.
- A two-administrator overlap supports safe handoff.
- Zero administrators is allowed but visibly reports
ACTIVE_NO_TENANT_ADMIN. - Ordinary multi-Tenant Person access remains intact.
- The deterministic starting administrator state is restored at the end.
If any of these fail, do not promote Bite 30H.
6. Technical promotion gate
Run this only after the UI manual suite passes.
6.1 Canonical automated gate
make local-check
Expected: pass.
6.2 Full Playwright
cd frontend
npx playwright test
cd ..
Expected: all tests pass.
The two E2E scenarios that genuinely create disposable Tenant Administrators must explicitly revoke their Role Grants during teardown. Actor deactivation alone is not sufficient cleanup under Bite 30H.
6.3 Clean migration chain through Bite 30H
Use a disposable database:
rm -f /tmp/ers-bite30h-migration.db
DB_PATH=/tmp/ers-bite30h-migration.db ./scripts/db-migrate.sh
Verify migration 000062_tenant_administrator_cardinality.up.sql is applied:
sqlite3 /tmp/ers-bite30h-migration.db \
"SELECT filename FROM schema_migrations WHERE filename LIKE '000062%';"
Expected:
000062_tenant_administrator_cardinality.up.sql
Then:
sqlite3 /tmp/ers-bite30h-migration.db "PRAGMA foreign_key_check;"
Expected: no rows.
Remove the disposable database when finished:
rm -f /tmp/ers-bite30h-migration.db
6.4 Read-only cardinality verifier on the manual-test database
List active Tenant Administrator assignments with canonical global Person
sqlite3 -header -column backend/data/app.db <<'SQL'
SELECT
g.tenant_id,
a.actor_key,
a.display_name,
a.active AS actor_active,
m.person_id AS global_person_id,
g.id AS grant_id
FROM authz_actor_role_grants g
JOIN authz_roles r
ON r.id = g.role_id
JOIN authz_actors a
ON a.id = g.actor_id
JOIN auth_account_actors aa
ON aa.actor_id = g.actor_id
AND aa.scope_type = 'TENANT'
AND aa.tenant_id = g.tenant_id
JOIN person_tenant_memberships m
ON m.id = aa.membership_id
AND m.tenant_id = aa.tenant_id
WHERE r.code = 'TENANT_ADMIN'
AND g.active = 1
ORDER BY g.tenant_id, a.actor_key;
SQL
At final restored state, expect exactly the deterministic Tenant A Operator D and Tenant B Operator E assignments.
Tenant 0..2 violation check
sqlite3 -header -column backend/data/app.db <<'SQL'
SELECT
g.tenant_id,
COUNT(*) AS active_tenant_admins
FROM authz_actor_role_grants g
JOIN authz_roles r ON r.id = g.role_id
WHERE r.code = 'TENANT_ADMIN'
AND g.active = 1
GROUP BY g.tenant_id
HAVING COUNT(*) > 2;
SQL
Expected: no rows.
Person 0..1-across-Tenants violation check
sqlite3 -header -column backend/data/app.db <<'SQL'
SELECT
m.person_id AS global_person_id,
COUNT(DISTINCT g.tenant_id) AS administered_tenants
FROM authz_actor_role_grants g
JOIN authz_roles r ON r.id = g.role_id
JOIN auth_account_actors aa
ON aa.actor_id = g.actor_id
AND aa.scope_type = 'TENANT'
AND aa.tenant_id = g.tenant_id
JOIN person_tenant_memberships m
ON m.id = aa.membership_id
AND m.tenant_id = aa.tenant_id
WHERE r.code = 'TENANT_ADMIN'
AND g.active = 1
GROUP BY m.person_id
HAVING COUNT(DISTINCT g.tenant_id) > 1;
SQL
Expected: no rows.
Distinct-Person-per-Tenant violation check
sqlite3 -header -column backend/data/app.db <<'SQL'
SELECT
g.tenant_id,
m.person_id AS global_person_id,
COUNT(*) AS active_grants_for_same_person
FROM authz_actor_role_grants g
JOIN authz_roles r ON r.id = g.role_id
JOIN auth_account_actors aa
ON aa.actor_id = g.actor_id
AND aa.scope_type = 'TENANT'
AND aa.tenant_id = g.tenant_id
JOIN person_tenant_memberships m
ON m.id = aa.membership_id
AND m.tenant_id = aa.tenant_id
WHERE r.code = 'TENANT_ADMIN'
AND g.active = 1
GROUP BY g.tenant_id, m.person_id
HAVING COUNT(*) > 1;
SQL
Expected: no rows.
Canonical Person-binding violation check
sqlite3 -header -column backend/data/app.db <<'SQL'
SELECT
g.id AS grant_id,
g.tenant_id,
g.actor_id
FROM authz_actor_role_grants g
JOIN authz_roles r ON r.id = g.role_id
WHERE r.code = 'TENANT_ADMIN'
AND g.active = 1
AND NOT EXISTS (
SELECT 1
FROM auth_account_actors aa
JOIN person_tenant_memberships m
ON m.id = aa.membership_id
AND m.tenant_id = aa.tenant_id
WHERE aa.actor_id = g.actor_id
AND aa.scope_type = 'TENANT'
AND aa.tenant_id = g.tenant_id
AND TRIM(COALESCE(m.person_id, '')) <> ''
);
SQL
Expected: no rows.
Bite 30H trigger presence
sqlite3 -header -column backend/data/app.db <<'SQL'
SELECT name
FROM sqlite_master
WHERE type = 'trigger'
AND name IN (
'trg_tenant_admin_global_person_insert',
'trg_tenant_admin_global_person_update',
'trg_tenant_admin_tenant_limit_insert',
'trg_tenant_admin_tenant_limit_update',
'trg_tenant_admin_distinct_person_insert',
'trg_tenant_admin_distinct_person_update',
'trg_tenant_admin_person_cross_tenant_insert',
'trg_tenant_admin_person_cross_tenant_update'
)
ORDER BY name;
SQL
Expected: 8 rows.
Foreign-key integrity
sqlite3 backend/data/app.db "PRAGMA foreign_key_check;"
Expected: no rows.
7. DevTools requirements
None.
No DevTools JavaScript helper is required for Bite 30H promotion. All functional cardinality behavior should be verifiable through the supported UI. The SQL above belongs only to the final technical promotion gate.
8. Final promotion checklist
Promote Bite 30H only when all are checked:
- Tests 01–10 pass from the deterministic fixture.
- Tenant A final state is Operator D only, 1/2,
ACTIVE_READY. - Tenant B final state is Operator E only, 1/2,
ACTIVE_READY. -
make local-checkpasses. - Full Playwright passes.
- Clean migration chain applies
000062successfully. - Tenant >2 verifier returns no rows.
- Person cross-Tenant verifier returns no rows.
- Same-Person/same-Tenant duplicate verifier returns no rows.
- Canonical Person-binding verifier returns no rows.
- All eight Bite 30H triggers are present.
-
PRAGMA foreign_key_checkreturns no rows. - No unresolved UI observability/cardinality defect remains.
If every item passes, Bite 30H is ready for promotion.