Bite 30D — Self‐Service and Delegated Authorization Refactor - RodrigoMattosoSilveira/EnterpriseRemoteSystems GitHub Wiki

Bite 30D — Self-Service and Delegated Authorization Refactor

Manual Verification Checklist Before Promotion

Issue: Bite #30D — Self-Service and Delegated Authorization Refactor

Branch: 708-bite-30d-self-service-and-delegated-authorization-refactor

Automated prerequisite

  • Migration 000048_self_service_delegated_authorization_refactor applied successfully.
  • make local-check passes.
  • Go tests pass.
  • Frontend tests pass.
  • Playwright E2E tests pass.

1. Scope of This Manual Verification

Bite 30D changes the authorization model from:

Authentication Account
    ↓
Actor
    ↓
PERSON Role Grant
    ↓
self-service permissions

to:

Authentication Account
    ↓
Tenant Actor
    ↓
ACTIVE Person–Tenant Membership
    ↓
Global Person
    ↓
intrinsic self-service permissions

Delegated Roles are now additive:

intrinsic self-service permissions
        +
delegated Role permissions
        =
effective permissions

Examples of delegated tenant Roles include:

TENANT_ADMIN
EARNINGS_OPERATOR
EXPENSE_OPERATOR

The promotion tests must therefore prove four major things:

  1. A Person no longer needs a PERSON Role Grant to use self-service.
  2. Collaborator self-service is derived intrinsically from current Collaborator state.
  3. Delegated Roles add authority without replacing or manufacturing self identity.
  4. Application-level authorization administration remains separate from tenant authority.

2. Important Scope Boundaries

Do not treat the following as Bite 30D promotion blockers unless 30D itself regressed previously working behavior.

Bite 30E

Final Account-owned tenant selection/session switching belongs to Bite 30E.

Do not require final tenant-switching UX or cache/session behavior beyond what already worked before 30D.

Bite 30H

Final removal of Application Administrator standing access to tenant business data and Tenant Support Access Leases belongs to Bite 30H.

For 30D, verify that Application authorization administration is application-scoped and that tenant Roles do not manufacture application authority.

Do not require the final 30H Application Administrator tenant-data isolation model yet.


3. Test Identities

Prepare or identify the following disposable test identities.

Identity A — Person-only Account

A Person with:

Authentication Account: ACTIVE
Person–Tenant Membership: ACTIVE
Collaborator Journey: none
Delegated Roles: none

Record:

Login:
Person ID:
Actor ID:
Tenant:

This is the most important 30D identity.


Identity B — Collaborator Account

A Person with:

Authentication Account: ACTIVE
Person–Tenant Membership: ACTIVE
Collaborator Journey: ACTIVE/current
Delegated Roles: none

Record:

Login:
Person ID:
Collaborator ID:
Actor ID:
Tenant:

Identity C — Tenant Administrator

A tenant-bound Actor with:

Person–Tenant Membership: ACTIVE
Delegated Role: TENANT_ADMIN

Record:

Login:
Actor ID:
Tenant:

Identity D — Earnings Operator

A tenant-bound Actor with:

Delegated Role: EARNINGS_OPERATOR

Identity E — Expenses Operator

A tenant-bound Actor with:

Delegated Role: EXPENSE_OPERATOR

Identity F — Application Administrator

Use the normal application-global administrator.

Expected:

scope = APPLICATION
roleCodes includes APPLICATION_ADMIN

Part A — Person Self-Service Is Intrinsic

4. Person-Only Account Can Sign In Without a PERSON Role

Sign in as

Identity A — Person-only Account.

Procedure

  • Sign in normally.
  • Allow the application to reach the Person's first permitted page.
  • Open the Person's own record.
  • Open any existing self-service history that the Person is entitled to see.

Expected

  • Login succeeds.
  • The Person can access their own Person record.
  • Previously allowed Person self-service remains available.
  • No authorization error occurs merely because there is no PERSON Role Grant.
  • The Actor is tenant scoped.
  • No delegated Role is required for basic Person self-service.

Promotion blocker: Person self-service disappears unless a PERSON Role is granted.


5. Inspect Current Actor for Person-Only Account

While signed in as Identity A, run:

fetch("/api/v1/authz/current-actor", {
  credentials: "include",
  headers: {
    "X-Tenant-ID": "<CURRENT-TENANT-ID>"
  }
})
  .then(r => r.json())
  .then(body => console.log(JSON.stringify(body, null, 2)));

Expected

The response should demonstrate the 30D separation.

Verify:

  • scope is tenant scoped.
  • The Person identity is present/resolved.
  • roleCodes does not need to contain PERSON.
  • intrinsicPermissions contains the Person's self-service permissions.
  • delegatedPermissions is empty, or contains only genuinely delegated permissions.
  • permissions contains the effective union.
  • Basic self-service works even when roleCodes is empty.

Record:

roleCodes: : null,
intrinsicPermissions: : [
      "authz.self.read",
      "people.self.read",
      "people.self.update",
      "reference_data.read",
      "tenants.read"
    ],
delegatedPermissions: []
permissions: [
      "authz.self.read",
      "people.self.read",
      "people.self.update",
      "reference_data.read",
      "tenants.read"
    ],

6. Database Spot Check — No Active PERSON Grant

For Identity A, inspect the Actor's Role Grants.

Example:

sqlite3 -header -column backend/data/app.db "
SELECT
    a.actor_key,
    r.code AS role_code,
    g.tenant_id,
    g.active
FROM authz_actors a
LEFT JOIN authz_actor_role_grants g
       ON g.actor_id = a.id
LEFT JOIN authz_roles r
       ON r.id = g.role_id
WHERE a.id = '<ACTOR-ID>'
ORDER BY r.code;
"

Expected

  • No active PERSON Role Grant exists.
  • No active SELF Role Grant exists.
  • Person self-service still works.

If a historical inactive PERSON grant remains:

  • It is inactive.
  • It is not required for authorization.

7. New Authentication Provisioning Does Not Create PERSON Grant

Use a disposable Person with:

ACTIVE Membership
Authentication: Not enabled

Procedure

  • Sign in as that tenant's Tenant Administrator.
  • Enable Authentication.
  • Complete the first-login password flow.
  • Sign in as the newly provisioned Person.
  • Verify their own Person page is accessible.
  • [ x Inspect their Role Grants.

Expected

  • Authentication Account is created/reused normally.
  • Person self-service works.
  • No active PERSON Role Grant is created.
  • No login-time backfill recreates the PERSON Role Grant.

Promotion blocker: provisioning or first login recreates PERSON as an authorization dependency.


8. Repeated Login Does Not Backfill PERSON Grant

Using Identity A:

  • Sign out.
  • Sign in again.
  • Refresh the application.
  • Inspect the Actor's Role Grants again.

Expected

  • No PERSON Role Grant appears.
  • Self-service still works.
  • Authorization remains stable across login/logout.

Part B — Membership Controls Intrinsic Person Self-Service

9. ACTIVE Membership Is Required

Use a disposable Person Account whose current tenant Membership can safely be deactivated for testing.

Record the Account and Actor first.

Procedure

  • Confirm the Account can initially use Person self-service.
  • Deactivate the Person–Tenant Membership using the supported administrative workflow.
  • Attempt to use the current tenant again.

Expected

  • The inactive Membership no longer establishes intrinsic self-service for that tenant.
  • ERS does not fall back to an old PERSON Role Grant.
  • ERS does not silently borrow another tenant's Membership or Actor.
  • No tenant data is exposed through the now-invalid identity chain.

Restore the Membership after the test if needed.

Promotion blocker: an inactive Membership still produces intrinsic self-service.


10. Membership Reactivation Restores Intrinsic Self-Service Without PERSON Grant

After Test 9:

  • Reactivate the Person–Tenant Membership.
  • Establish a fresh authenticated session if necessary.
  • Open the Person's self-service page again.

Expected

  • Person self-service is restored.
  • No PERSON Role Grant is created.
  • The same global Person and tenant Actor continue to be used.

Part C — Collaborator Self-Service Is Intrinsic

11. Current Collaborator Gets Collaborator Self-Service Without Delegated Role

Sign in as

Identity B — Collaborator Account with no delegated Roles.

Procedure

  • Sign in.
  • Open the Person's own record.
  • Open the Collaborator's current work assignment/current-work area.
  • Open current earnings/expenses/current-account functions that are part of Collaborator self-service.

Expected

  • Person self-service is available.
  • Current Collaborator self-service is additionally available.
  • No PERSON Role Grant is needed.
  • No operator Role such as EARNINGS_OPERATOR or EXPENSE_OPERATOR is required for self-service views.
  • The Collaborator cannot administer unrelated tenant data merely because they are a Collaborator.

12. Inspect Collaborator Current Actor

Run /api/v1/authz/current-actor as Identity B.

Expected

  • Collaborator self-service permissions appear under intrinsicPermissions.
  • Operator/admin permissions do not appear unless actually delegated.
  • roleCodes does not need a Collaborator/self-service Role.
  • permissions is the union of intrinsic plus any delegated permissions.

13. Ending Collaborator Journey Removes Current Collaborator Intrinsic Authority

Use a disposable Collaborator.

Procedure

  • Confirm current Collaborator self-service works.
  • End/discontinue the current Collaborator Journey using the normal administrative workflow.
  • Establish a fresh session if necessary.
  • Revisit current Collaborator functions.

Expected

  • Person self-service remains.
  • Current Collaborator-only self-service is no longer available.
  • [ x Historical Person access remains according to existing rules.
  • ERS does not require deleting the Authentication Account or Person Actor.
  • No Role Grant must be manually revoked merely to end Collaborator self-service.

Promotion blocker: ended Collaborator status still grants current Collaborator intrinsic authority.


Part D — Delegated Roles Are Additive

14. Tenant Administrator Retains Person Self-Service Plus Delegated Administration

Sign in as

Identity C — Tenant Administrator.

Procedure

  • Open the administrator's own Person/self-service record.
  • Perform a normal Tenant Administrator operation.
  • Query /api/v1/authz/current-actor.

Expected

  • Person intrinsic permissions are present.
  • TENANT_ADMIN appears in roleCodes.
  • Tenant-admin permissions appear under delegatedPermissions.
  • Effective permissions contains both intrinsic and delegated permissions.
  • Delegated administration does not replace the administrator's own Person identity.

Conceptually:

Person intrinsic permissions
        +
TENANT_ADMIN delegated permissions
        =
effective Tenant Administrator authority

15. Earnings Operator Retains Self-Service But Gets Only Earnings Delegation

Sign in as

Identity D — Earnings Operator.

Expected

  • Own Person self-service works.
  • Earnings operator functions work.
  • Unrelated Expenses administration remains unavailable unless separately delegated.
  • Authorization administration remains unavailable.
  • EARNINGS_OPERATOR is delegated, not intrinsic.

16. Expenses Operator Retains Self-Service But Gets Only Expenses Delegation

Sign in as

Identity E — Expenses Operator.

Expected

  • Own Person self-service works.
  • Expense operator functions work.
  • Unrelated Earnings administration remains unavailable unless separately delegated.
  • Authorization administration remains unavailable.
  • EXPENSE_OPERATOR is delegated, not intrinsic.

17. Removing a Delegated Role Does Not Remove Self-Service

Use a disposable tenant-bound Actor with a delegated operator Role.

Procedure

  • Confirm self-service works.
  • Confirm the delegated operator function works.
  • Revoke the delegated Role.
  • Establish a fresh session if necessary.
  • Retry both functions.

Expected

  • Delegated operator function is no longer available.
  • Person self-service still works.
  • Person identity remains intact.
  • Authentication Account remains intact.
  • No PERSON Role is created as a fallback.

Promotion blocker: revoking an operator/admin Role also destroys ordinary Person self-service.


Part E — Tenant Role Grant Invariants

18. Tenant Role Can Be Granted to a Properly Tenant-Bound Actor

Sign in as

Application Administrator using /admin/authorization.

Use an Actor that is:

bound to an Authentication Account
+
bound to an ACTIVE Membership
+
bound to the same target tenant

Procedure

  • Select a valid tenant Role such as TENANT_ADMIN, EARNINGS_OPERATOR, or EXPENSE_OPERATOR.
  • Grant it for the Actor's tenant.

Expected

  • Grant succeeds.
  • Grant appears active.
  • The Role becomes effective for that tenant.
  • No duplicate or malformed grant is created.

19. Tenant Role Cannot Be Granted to a Detached Actor

Use or create an authorization Actor that is not bound through an Authentication Account to an active Membership in the target tenant.

Procedure

Attempt to grant:

TENANT_ADMIN

or another tenant Role.

Expected

Preferably the UI prevents the operation or explains the requirement.

If submitted directly:

  • Backend rejects the grant cleanly.
  • No malformed Role Grant is persisted.
  • No 500 error occurs.

Expected conceptual rule:

tenant Role
    requires
Actor bound to that same tenant

Promotion blocker: detached Actors can acquire tenant delegated authority.


20. Tenant Role Cannot Be Granted for a Different Tenant

Use an Actor bound to Tenant A.

Attempt to grant a tenant Role scoped to Tenant B.

Expected

  • Operation is rejected.
  • Actor does not acquire Tenant-B authority.
  • Existing Tenant-A identity remains unchanged.
  • No cross-tenant grant is persisted.

Promotion blocker: Actor bound to Tenant A can receive a Tenant-B delegated Role.


21. PERSON/SELF Role Cannot Be Granted

Sign in as

Application Administrator on /admin/authorization.

Procedure

Inspect available Roles for a new grant.

Expected

  • PERSON is absent from grantable Roles, disabled, or clearly historical/inactive.
  • SELF is not grantable.
  • A direct API attempt to grant either Role is rejected.
  • Existing historical inactive records may remain for audit/migration history.

Promotion blocker: administrators can recreate PERSON or SELF as active delegated Roles.


22. Existing Invalid Legacy PERSON Grants Are Inactive

Inspect /admin/authorization and/or the database.

Expected

  • Legacy PERSON Role is inactive.
  • Historical grants are inactive.
  • Historical records were not unnecessarily destroyed if retained for audit/history.
  • They do not affect effective permissions.

Part F — Self Permission Cannot Be Spoofed Through Delegation

23. Delegated Permission Does Not Manufacture Self Identity

This validates an important 30D security invariant.

Use a tenant Actor that has delegated authority but does not represent the target Person.

Procedure

Attempt to access another Person's self-only resource using:

  • a delegated Role;
  • or any effective permission that happens to have the same permission name as a self permission.

Expected

  • Request is denied unless the resource actually belongs to the authenticated Person identity.
  • ERS validates Person identity in addition to permission.
  • Delegated authority cannot turn another Person's resource into "self".

Promotion blocker: possessing a delegated/effective permission is sufficient to impersonate another Person's self-service identity.


24. Own Self Route Still Works Through Intrinsic Permission

Using Identity A:

  • Access the Person's own self-service resource.

Expected

  • Request succeeds.
  • Success comes from intrinsic identity/permission.
  • No delegated Role is required.

Together Tests 23 and 24 establish:

self authorization
=
correct intrinsic permission
+
correct authenticated Person identity

not merely:

self authorization
=
permission string exists somewhere

Part G — Application Authorization Administration

25. Application Administrator Can Administer Authorization

Sign in as

Identity F — Application Administrator.

Procedure

Open:

/admin/authorization

Perform safe operations such as:

  • list Actors;
  • list Roles;
  • inspect Role Grants;
  • create/revoke a disposable valid delegated tenant Role Grant.

Expected

  • Authorization Administration loads.
  • Application Administrator can perform supported control-plane operations.
  • Current Actor remains application scoped.

Check:

fetch("/api/v1/authz/current-actor", {
  credentials: "include",
  headers: {
    "X-Tenant-ID": "default"
  }
})
  .then(r => r.json())
  .then(body => console.log(JSON.stringify(body, null, 2)));

Expected approximately:

scope = APPLICATION
roleCodes includes APPLICATION_ADMIN

26. Tenant Administrator Cannot Administer Application Authorization

Sign in as

Identity C — Tenant Administrator.

Procedure

Attempt to open:

/admin/authorization

and, if practical, directly call an authorization-administration API.

Expected

  • Application Authorization Administration is unavailable or denied.
  • Direct API request is rejected with 403 or equivalent.
  • Tenant Administrator authority does not become Application scope.
  • Tenant Administrator cannot manage the global Role/permission catalog.

Promotion blocker: TENANT_ADMIN can administer application-global authorization.


27. Tenant Administrator Remains Tenant Scoped

As Identity C, run /api/v1/authz/current-actor.

Expected

scope = TENANT
roleCodes includes TENANT_ADMIN

Verify:

  • Actor is tenant scoped.
  • TENANT_ADMIN appears as delegated authority.
  • Application control-plane permissions are absent.

Part H — Regression Checks

28. Authentication Account Provisioning Still Works

Use a disposable Person with active Membership and no Authentication Account.

Procedure

  • Enable Authentication.
  • Complete required password change.
  • Sign in.

Expected

  • Authentication works.
  • Correct tenant Actor resolves.
  • Person self-service works.
  • No active PERSON Role Grant is created.

29. Multi-Tenant Authentication Account Still Works

Use an Account already bound to two tenant Actors.

Procedure

Resolve current Actor for Tenant A and Tenant B using the existing supported mechanism.

Expected

  • Same Authentication Account remains in use.
  • Tenant A resolves Actor A.
  • Tenant B resolves Actor B.
  • 30D did not duplicate Accounts or Actors.
  • Intrinsic permissions are derived from the Membership associated with the resolved Actor.

Do not require final Bite 30E tenant-switching UX behavior.


30. Account Deactivation/Reactivation Still Preserves Authorization Bindings

Use a disposable Account.

Procedure

  • Record Actor bindings and delegated Roles.
  • Deactivate the Account.
  • Reactivate it through the supported 30C.2 recovery workflow.
  • Sign in again.

Expected

  • Same Account remains.
  • Same Actors remain.
  • Same valid delegated Role Grants remain.
  • No PERSON Role is recreated.
  • Intrinsic self-service returns when the valid Account/Actor/Membership identity chain is restored.

31. Browser Console and Network Sanity Check

During Tests 4–30:

  • Keep DevTools Console open.
  • Review failed Network requests.

Expected

  • No unexpected JavaScript exceptions.
  • No unexpected 500.
  • Expected authorization failures return clean 401/403.
  • Invalid tenant Role Grants return validation/authorization errors rather than database crashes.
  • No cross-tenant authority leak occurs.

32. Backend Log Sanity Check

After manual testing, review the backend log for unexpected:

panic
internal API error
foreign key
constraint failed
role grant
authorization
intrinsic
delegated

Expected

  • No panic.
  • No unexpected internal errors.
  • No unexpected grant-scope constraint failures during valid operations.
  • Deliberately invalid grant tests fail cleanly.

33. Database Integrity Check

Run:

sqlite3 backend/data/app.db "
PRAGMA foreign_key_check;
"

Expected

No rows returned.


34. Database Spot Check — PERSON Role Is Inactive

Run:

sqlite3 -header -column backend/data/app.db "
SELECT
    id,
    code,
    name,
    active
FROM authz_roles
WHERE UPPER(code) IN ('PERSON', 'SELF');
"

Expected

Any legacy PERSON/SELF role that remains:

active = 0

35. Database Spot Check — No Active PERSON/SELF Grants

Run:

sqlite3 -header -column backend/data/app.db "
SELECT
    g.id,
    g.actor_id,
    r.code AS role_code,
    g.tenant_id,
    g.active
FROM authz_actor_role_grants g
JOIN authz_roles r
  ON r.id = g.role_id
WHERE UPPER(r.code) IN ('PERSON', 'SELF')
  AND g.active = 1;
"

Expected

No rows returned.


36. Database Spot Check — Tenant Grants Match Actor Bindings

Run:

sqlite3 -header -column backend/data/app.db "
SELECT
    g.id AS grant_id,
    g.actor_id,
    r.code AS role_code,
    g.tenant_id AS grant_tenant,
    aaa.tenant_id AS actor_binding_tenant
FROM authz_actor_role_grants g
JOIN authz_roles r
  ON r.id = g.role_id
LEFT JOIN auth_account_actors aaa
  ON aaa.actor_id = g.actor_id
 AND aaa.scope_type = 'TENANT'
 AND aaa.tenant_id = g.tenant_id
WHERE g.active = 1
  AND g.tenant_id <> '*'
  AND UPPER(r.code) NOT IN ('PERSON', 'SELF')
  AND aaa.actor_id IS NULL;
"

Expected

No rows returned.

Any row here would indicate an active tenant delegated Role Grant without a corresponding same-tenant Actor binding.


Promotion Gate

Bite 30D is ready for promotion when all of the following are true:

  • Person-only Account can use self-service with no active PERSON Role Grant.
  • current-actor clearly separates intrinsic, delegated, and effective permissions.
  • New Authentication provisioning does not create a PERSON Role Grant.
  • Login does not backfill a PERSON Role Grant.
  • ACTIVE Person–Tenant Membership is required for intrinsic Person self-service.
  • Membership reactivation restores intrinsic Person self-service without creating PERSON.
  • Current Collaborator self-service is intrinsic.
  • Ending Collaborator status removes current Collaborator-only intrinsic authority while preserving Person self-service.
  • TENANT_ADMIN is additive delegated authority.
  • EARNINGS_OPERATOR is additive delegated authority.
  • EXPENSE_OPERATOR is additive delegated authority.
  • Revoking a delegated Role does not remove ordinary Person self-service.
  • Valid tenant Role Grant succeeds only for an Actor bound to that same tenant.
  • Detached Actor cannot acquire tenant delegated authority.
  • Actor bound to Tenant A cannot acquire a Tenant-B delegated Role.
  • PERSON and SELF cannot be newly granted.
  • Existing historical PERSON/SELF grants are inactive.
  • Delegated permissions cannot spoof another Person's self identity.
  • Own self routes work from intrinsic authorization.
  • Application Administrator can administer application authorization.
  • Tenant Administrator cannot administer application-global authorization.
  • Tenant Administrator remains tenant scoped.
  • Existing Authentication provisioning still works.
  • Existing multi-tenant Authentication Account behavior still works.
  • Account deactivation/reactivation preserves Actor/delegated-role bindings without recreating PERSON.
  • No unexpected 500, panic, or cross-tenant authorization leak occurs.
  • PRAGMA foreign_key_check returns no rows.
  • No active PERSON/SELF Role Grants remain.
  • No active tenant Role Grant exists without a matching same-tenant Actor binding.

Promotion-Blocking Defects

Do not promote Bite 30D if any of these occur:

  • A Person loses self-service merely because no PERSON Role Grant exists.
  • Authentication provisioning or login recreates an active PERSON Role Grant.
  • An inactive Person–Tenant Membership still yields intrinsic self-service.
  • Collaborator current self-service requires an operator/admin Role.
  • An ended Collaborator Journey continues to provide current Collaborator intrinsic authority.
  • Revoking an operator/admin Role also destroys ordinary Person self-service.
  • A detached Actor can receive a tenant Role.
  • A Tenant-A Actor can receive a Tenant-B Role Grant.
  • PERSON or SELF can be newly granted.
  • A delegated permission can impersonate another Person's self-service identity.
  • A Tenant Administrator can administer the application-global authorization catalog.
  • A tenant Actor resolves as Application scope.
  • 30D creates duplicate Accounts or Actors.
  • Account recovery recreates PERSON grants.
  • Any cross-tenant authorization leak is observed.
  • Any unexpected database integrity violation, panic, or 500 occurs.

If all promotion-gate items pass, Bite 30D — Self-Service and Delegated Authorization Refactor is ready for promotion.

⚠️ **GitHub.com Fallback** ⚠️