Files
konstruct/.planning/phases/04-rbac/04-VERIFICATION.md

15 KiB

phase, verified, status, score, re_verification
phase verified status score re_verification
04-rbac 2026-03-24T23:22:44Z passed 18/18 must-haves verified false

Phase 4: RBAC Verification Report

Phase Goal: Three-tier role-based access control — platform admins manage the SaaS, customer admins manage their tenant, customer operators get read-only access — with email invitation flow for onboarding tenant users Verified: 2026-03-24T23:22:44Z Status: PASSED Re-verification: No — initial verification


Goal Achievement

Observable Truths

Plan 01 truths:

# Truth Status Evidence
1 Platform admin caller gets 200 on any tenant endpoint; non-admin gets 403 VERIFIED rbac.py raises HTTP_403_FORBIDDEN for non-platform_admin; 11 unit tests pass
2 Customer admin gets 200 on their own tenant endpoints; gets 403 on other tenants VERIFIED require_tenant_admin checks UserTenantRole membership; test_tenant_admin_own_tenant / test_tenant_admin_no_membership pass
3 Customer operator gets 403 on mutating endpoints; gets 200 on read-only endpoints VERIFIED require_tenant_admin always rejects operator; require_tenant_member allows operator on GET paths
4 Invite token with valid HMAC and unexpired timestamp validates successfully VERIFIED test_token_roundtrip passes; hmac.compare_digest + 48h TTL enforced in invite_token.py
5 Invite token with tampered signature or expired timestamp raises ValueError VERIFIED test_token_tamper_rejected and test_token_expired_rejected pass
6 Auth verify response returns role + tenant_ids instead of is_admin VERIFIED AuthVerifyResponse has role, tenant_ids, active_tenant_id; is_admin absent from PortalUser model

Plan 02 truths:

# Truth Status Evidence
7 JWT token contains role, tenant_ids, and active_tenant_id after login VERIFIED auth.ts jwt callback sets token.role, token.tenant_ids, token.active_tenant_id (lines 72-74)
8 Customer operator navigating to /billing is silently redirected to /agents VERIFIED proxy.ts line 57: if (role === "customer_operator") redirects restricted paths to /agents
9 Customer operator does not see Billing, API Keys, or User Management in sidebar VERIFIED nav.tsx filters navItems by allowedRoles array via useSession()
10 Customer admin sees tenant dashboard after login VERIFIED proxy.ts landing page logic: customer_admin routes to /dashboard
11 Platform admin sees platform overview with tenant picker after login VERIFIED proxy.ts case "platform_admin" routes to /dashboard; TenantSwitcher rendered in layout
12 Multi-tenant user can switch active tenant without logging out VERIFIED tenant-switcher.tsx calls update({ active_tenant_id: newTenantId }) (line 71); auth.ts jwt callback handles trigger === "update"
13 Impersonation shows a visible banner with exit button VERIFIED impersonation-banner.tsx 49 lines, amber banner with exit button; integrated in layout.tsx
14 Invite acceptance page accepts token, lets user set password, creates account VERIFIED app/invite/[token]/page.tsx (172 lines) outside (dashboard) group; POSTs to /api/portal/invitations/accept
15 User management page lists users for tenant with invite button VERIFIED app/(dashboard)/users/page.tsx (411 lines) with invite dialog and resend capability
16 Platform admin global user page shows all users across all tenants VERIFIED app/(dashboard)/admin/users/page.tsx (462 lines) with tenant/role filters

Plan 03 truths:

# Truth Status Evidence
17 Every mutating portal API endpoint returns 403 for customer_operator VERIFIED 16 Depends(require_tenant_admin) / Depends(require_platform_admin) guards across portal.py, billing.py, channels.py, llm_keys.py
18 Customer operator gets 200 on POST agent test-message endpoint VERIFIED POST /tenants/{tid}/agents/{aid}/test uses Depends(require_tenant_member) (line 622 portal.py)

Score: 18/18 truths verified


Required Artifacts

Plan 01 Artifacts

Artifact Status Details
migrations/versions/006_rbac_roles.py VERIFIED 220 lines; adds user_tenant_roles, portal_invitations, drops is_admin
packages/shared/shared/api/rbac.py VERIFIED 5473 bytes; exports PortalCaller, get_portal_caller, require_platform_admin, require_tenant_admin, require_tenant_member
packages/shared/shared/api/invitations.py VERIFIED 11844 bytes; exports invitations_router with create/accept/resend/list endpoints
packages/shared/shared/invite_token.py VERIFIED 2745 bytes; exports generate_invite_token, validate_invite_token, token_to_hash
packages/shared/shared/email.py VERIFIED 3479 bytes; exports send_invite_email
tests/unit/test_rbac_guards.py VERIFIED 188 lines (min 50); 11 tests all passing
tests/unit/test_invitations.py VERIFIED 368 lines (min 40); 11 tests all passing
tests/unit/test_portal_auth.py VERIFIED 279 lines (min 30); 7 tests all passing; 27 total unit tests pass

Plan 02 Artifacts

Artifact Status Details
packages/portal/lib/auth-types.ts VERIFIED 38 lines; declare module "next-auth" augmentation present
packages/portal/lib/auth.ts VERIFIED 3473 bytes; token.role set in jwt callback
packages/portal/proxy.ts VERIFIED 3532 bytes; customer_operator handling present
packages/portal/components/nav.tsx VERIFIED 3656 bytes; useSession imported and used for role filtering
packages/portal/components/tenant-switcher.tsx VERIFIED 96 lines (min 30); update.*active_tenant_id present
packages/portal/components/impersonation-banner.tsx VERIFIED 49 lines (min 15); amber banner with exit
packages/portal/app/invite/[token]/page.tsx VERIFIED 172 lines (min 40); outside (dashboard) group — no auth required
packages/portal/app/(dashboard)/users/page.tsx VERIFIED 411 lines (min 40); invite dialog + resend
packages/portal/app/(dashboard)/admin/users/page.tsx VERIFIED 462 lines (min 40); cross-tenant filters

Plan 03 Artifacts

Artifact Status Details
tests/integration/test_portal_rbac.py VERIFIED 949 lines (min 80); X-Portal-User-Role headers used throughout
tests/integration/test_invite_flow.py VERIFIED 484 lines (min 40)

From To Via Status Evidence
packages/shared/shared/api/rbac.py packages/shared/shared/models/auth.py imports UserTenantRole WIRED Line 32: from shared.models.auth import UserTenantRole
packages/shared/shared/api/invitations.py packages/shared/shared/invite_token.py generates and validates HMAC tokens WIRED Line 40: from shared.invite_token import generate_invite_token, token_to_hash, validate_invite_token
packages/shared/shared/api/portal.py packages/shared/shared/models/auth.py auth/verify returns role + tenant_ids WIRED Lines 48, 284-303: tenant_ids resolved and returned in response
From To Via Status Evidence
packages/portal/lib/auth.ts /api/portal/auth/verify fetch in authorize(), receives role + tenant_ids WIRED Lines 55-56, 72-74: token.role, token.tenant_ids set from response
packages/portal/proxy.ts packages/portal/lib/auth.ts reads session.user.role for redirect logic WIRED Line 47: const role = (session.user as { role?: string }).role
packages/portal/components/nav.tsx next-auth/react useSession() to read role for nav filtering WIRED Line 13 import, line 90 use: const { data: session } = useSession()
packages/portal/components/tenant-switcher.tsx next-auth/react update() to change active_tenant_id in JWT WIRED Line 71: await update({ active_tenant_id: newTenantId })
From To Via Status Evidence
packages/shared/shared/api/portal.py packages/shared/shared/api/rbac.py Depends(require_*) on all endpoints WIRED 16 Depends(require_*) declarations across portal endpoints
packages/shared/shared/api/billing.py packages/shared/shared/api/rbac.py Depends(require_tenant_admin) on billing endpoints WIRED Lines 209, 259
tests/integration/test_portal_rbac.py packages/shared/shared/api/rbac.py Tests pass role headers and assert 403/200 WIRED Lines 68, 76, 85: X-Portal-User-Role header set per role

Requirements Coverage

Requirement Description Plans Status Evidence
RBAC-01 Platform admin role with full access to all tenants, agents, users, and platform settings 01, 02, 03 SATISFIED require_platform_admin guards; platform_admin bypasses all tenant checks; 16 guarded endpoints
RBAC-02 Customer admin role scoped to a single tenant with full control over agents, channels, billing, API keys, and user management 01, 03 SATISFIED require_tenant_admin on all mutating tenant endpoints; cross-tenant 403 enforced
RBAC-03 Customer operator role scoped to a single tenant with read-only access to agents, conversations, and usage dashboards 01, 03 SATISFIED require_tenant_member on GET endpoints; require_tenant_admin blocks operator on mutations; operator CAN send test messages
RBAC-04 Customer admin can invite users by email — invitee receives activation link to set password 01, 02, 03 SATISFIED Full invitation system: HMAC tokens, SMTP email, invitations_router, invite acceptance page at /invite/[token]
RBAC-05 Portal navigation, pages, and UI elements adapt based on user role 02 SATISFIED Role-filtered nav, proxy redirects, impersonation banner, tenant switcher — all present and wired
RBAC-06 API endpoints enforce role-based authorization — unauthorized actions return 403 01, 03 SATISFIED FastAPI Depends() guards on all 17+ endpoints; integration tests cover full role matrix

All 6 requirements satisfied. No orphaned requirements.


Anti-Patterns Found

File Line Pattern Severity Impact
packages/shared/shared/api/portal.py ~630 Agent test-message returns stub echo response Info Documented decision: full orchestrator wiring deferred; endpoint has correct RBAC, stub response only

No blocker or warning-level anti-patterns found. The test-message stub is a documented, intentional deferral — RBAC enforcement (the goal of this phase) is correct.


Human Verification Required

The following items cannot be verified programmatically. All automated checks passed; these items require a running environment.

1. End-to-end Invitation Flow in Browser

Test: Start dev environment, create invitation as customer_admin, open invite URL in incognito, set password, log in as new user Expected: Account created with correct role and tenant membership; JWT claims match; login succeeds Why human: Full HTTP + DB + email path requires live services

2. Operator Path Redirect in Browser

Test: Log in as customer_operator, navigate to /billing directly Expected: Silently redirected to /agents with no error page shown Why human: Proxy behavior requires running Next.js server

3. Tenant Switcher Context Switch

Test: Log in as user with multiple tenant memberships, use tenant switcher dropdown Expected: Active tenant changes instantly without page reload; TanStack Query refetches data for new tenant Why human: Requires live JWT update flow and visible UI state change

4. Impersonation Banner Display

Test: Log in as platform_admin, impersonate a tenant via /admin/impersonate Expected: Amber banner appears at top of viewport showing tenant name with visible "Exit" button; banner disappears after exit Why human: Visual UI element, requires live session with impersonating_tenant_id JWT claim

5. Integration Tests Against Live DB

Test: uv run pytest tests/integration/test_portal_rbac.py tests/integration/test_invite_flow.py -x -v Expected: All 56 integration tests pass (currently skipped in CI due to no DB) Why human: Requires PostgreSQL with migration 006 applied


Commit Verification

Commit Description Verified
f710c9c feat(04-rbac-01): DB migration 006 + RBAC ORM models + config fields Present in main repo
d59f85c feat(04-rbac-01): RBAC guards + invite token + email + invitation API Present in main repo
7b0594e test(04-rbac-01): unit tests for RBAC guards, invitation system, portal auth Present in main repo
43b73aa feat(04-rbac-03): wire RBAC guards to all portal API endpoints + new endpoints Present in main repo
9515c53 test(04-rbac-03): add failing integration tests for RBAC enforcement and invite flow Present in main repo
fcb1166 feat(04-rbac-02): Auth.js JWT update, role-filtered nav, tenant switcher, impersonation banner Present in portal submodule
744cf79 feat(04-rbac-02): invite acceptance page, per-tenant users page, platform admin users page Present in portal submodule

Note: Portal package (packages/portal) is a git submodule. Plan 02 commits exist in the submodule's history. The submodule has uncommitted working tree changes (portal listed as modified in parent repo status).


Summary

Phase 4 goal is achieved. All three tiers of role-based access control exist and are wired:

  • DB layer: Migration 006 adds role column to portal_users (TEXT+CHECK), user_tenant_roles table, and portal_invitations table. is_admin is gone.
  • Backend guards: require_platform_admin, require_tenant_admin, require_tenant_member implemented with real 403 enforcement and platform_admin bypass logic. Guards wired to all 17+ API endpoints across 5 routers.
  • Invitation system: HMAC-SHA256 tokens with 48h TTL, token hash stored (never raw token), SMTP email utility, full CRUD API (create/accept/resend/list), Celery task for async email dispatch.
  • Portal JWT: Auth.js carries role, tenant_ids, active_tenant_id replacing is_admin. Tenant switcher updates JWT mid-session via trigger: "update".
  • Portal routing: Proxy silently redirects customer_operator from restricted paths. /invite/[token] is public (outside dashboard group).
  • Portal UI: Nav hides items by role. Impersonation banner is present. User management pages exist for both tenant and platform scope.
  • Tests: 27 unit tests pass. 56 integration tests written (require live DB to run — documented in summaries).

The one notable deferred item (test-message endpoint returns echo stub pending orchestrator integration) is a documented decision and does not block the RBAC goal.


Verified: 2026-03-24T23:22:44Z Verifier: Claude (gsd-verifier)