---
phase: 04-rbac
plan: 01
subsystem: rbac
tags: [rbac, auth, invitations, migration, orm, guards]
dependency_graph:
  requires: []
  provides:
    - RBAC guard dependencies (require_platform_admin, require_tenant_admin, require_tenant_member)
    - Migration 006 (user_tenant_roles, portal_invitations tables)
    - HMAC invite token generation and validation
    - Invitation CRUD API (create, accept, resend, list)
    - SMTP email utility for invitations
    - auth/verify returning role + tenant_ids (replaces is_admin)
  affects:
    - packages/shared/shared/models/auth.py (PortalUser.is_admin removed)
    - packages/shared/shared/api/portal.py (AuthVerifyResponse shape changed)
    - packages/gateway/gateway/main.py (invitations_router mounted)
tech_stack:
  added: []
  patterns:
    - TEXT + CHECK constraint for role column (per Phase 1 ADR, avoids sa.Enum DDL issues)
    - HMAC-SHA256 with hmac.compare_digest for timing-safe token verification
    - SHA-256(token) stored in DB — raw token never persisted
    - Celery fire-and-forget via lazy local import in API handler (avoids circular dep)
    - platform_admin bypasses all tenant membership checks (no DB query)
key_files:
  created:
    - migrations/versions/006_rbac_roles.py
    - packages/shared/shared/api/rbac.py
    - packages/shared/shared/invite_token.py
    - packages/shared/shared/email.py
    - packages/shared/shared/api/invitations.py
    - tests/unit/test_rbac_guards.py
    - tests/unit/test_invitations.py
    - tests/unit/test_portal_auth.py
  modified:
    - packages/shared/shared/models/auth.py
    - packages/shared/shared/api/portal.py
    - packages/shared/shared/api/__init__.py
    - packages/shared/shared/config.py
    - packages/gateway/gateway/main.py
    - packages/orchestrator/orchestrator/tasks.py
decisions:
  - "Role stored as TEXT + CHECK (not sa.Enum) — per Phase 1 ADR to avoid Alembic DDL conflicts"
  - "SHA-256 hash of raw token stored in DB — token_hash enables O(1) lookup without exposing token"
  - "platform_admin bypasses tenant membership check without DB query — simpler and faster"
  - "Celery task dispatch uses lazy local import in invitations.py — avoids shared->orchestrator circular dep"
  - "portal_url reused for invite link construction — not duplicated as portal_base_url"
metrics:
  duration: "8 minutes"
  completed: "2026-03-24"
  tasks_completed: 3
  files_created: 8
  files_modified: 6
---

# Phase 4 Plan 01: RBAC Foundation Summary

**One-liner:** 3-tier RBAC (platform_admin/customer_admin/customer_operator) with DB migration, FastAPI guard dependencies, HMAC invite tokens, and invite-only onboarding API.

## What Was Built

### Task 1: DB Migration + ORM Models + Config
Migration 006 (`migrations/versions/006_rbac_roles.py`) adds the RBAC schema to PostgreSQL:
- Adds `role TEXT + CHECK` column to `portal_users`, backfills `is_admin` values, drops `is_admin`
- Creates `user_tenant_roles` table (user_id FK, tenant_id FK, UNIQUE constraint)
- Creates `portal_invitations` table (token_hash UNIQUE, status, expires_at, all FKs)

`packages/shared/shared/models/auth.py` gains:
- `UserRole` string enum (PLATFORM_ADMIN, CUSTOMER_ADMIN, CUSTOMER_OPERATOR)
- `UserTenantRole` ORM model with CASCADE deletes
- `PortalInvitation` ORM model
- `PortalUser.role` replaces `PortalUser.is_admin`

`packages/shared/shared/config.py` gains: `invite_secret`, `smtp_host`, `smtp_port`, `smtp_username`, `smtp_password`, `smtp_from_email`.

### Task 2: RBAC Guards + Invite Token + Email + Invitation API

**`packages/shared/shared/api/rbac.py`** — FastAPI dependency guards:
- `PortalCaller` dataclass (user_id, role, tenant_id from request headers)
- `get_portal_caller` — parses X-Portal-User-Id/Role/Tenant-Id headers, raises 401 on bad UUID
- `require_platform_admin` — raises 403 for non-platform_admin
- `require_tenant_admin` — platform_admin bypasses; customer_admin checked against UserTenantRole; operator always 403
- `require_tenant_member` — platform_admin bypasses; customer_admin/operator checked against UserTenantRole

**`packages/shared/shared/invite_token.py`** — HMAC token utilities:
- `generate_invite_token(invitation_id)` — HMAC-SHA256, base64url-encoded, embeds `{id}:{timestamp}`
- `validate_invite_token(token)` — timing-safe compare_digest, 48h TTL check, returns invitation_id
- `token_to_hash(token)` — SHA-256 hex digest for DB storage

**`packages/shared/shared/email.py`** — SMTP email sender (sync, for Celery):
- Sends HTML+text multipart invite email
- Skips silently if smtp_host is empty (dev-friendly)

**`packages/shared/shared/api/invitations.py`** — Invitation CRUD router:
- `POST /api/portal/invitations` — create invitation (requires tenant admin), returns raw token
- `POST /api/portal/invitations/accept` — validate token, create PortalUser + UserTenantRole, mark accepted
- `POST /api/portal/invitations/{id}/resend` — regenerate token, extend expiry
- `GET /api/portal/invitations` — list pending invitations for caller's tenant

**`packages/shared/shared/api/portal.py`** — auth/verify updated:
- `AuthVerifyResponse` now returns `role`, `tenant_ids`, `active_tenant_id` (replaced `is_admin`)
- platform_admin returns all tenant IDs; customer roles return their UserTenantRole tenant IDs
- `/auth/register` gated behind `require_platform_admin` with deprecation comment

**`packages/orchestrator/orchestrator/tasks.py`** — added `send_invite_email_task` Celery task.

**`packages/gateway/gateway/main.py`** — `invitations_router` mounted.

### Task 3: Unit Tests (27 passing)

- `tests/unit/test_rbac_guards.py` (11 tests): RBAC guard pass/reject scenarios, platform_admin bypass
- `tests/unit/test_invitations.py` (11 tests): HMAC token roundtrip, tamper/expiry, invitation CRUD
- `tests/unit/test_portal_auth.py` (7 tests): auth/verify returns role+tenant_ids+active_tenant_id

## Deviations from Plan

None — plan executed exactly as written.

## Commits

| Hash | Description |
|------|-------------|
| f710c9c | feat(04-rbac-01): DB migration 006 + RBAC ORM models + config fields |
| d59f85c | feat(04-rbac-01): RBAC guards + invite token + email + invitation API |
| 7b0594e | test(04-rbac-01): unit tests for RBAC guards, invitation system, portal auth |

## Self-Check: PASSED

All files created and verified before this summary was written.
