Files
konstruct/.planning/phases/04-rbac/04-01-SUMMARY.md

6.2 KiB

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
04-rbac 01 rbac
rbac
auth
invitations
migration
orm
guards
requires provides affects
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)
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)
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)
created modified
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
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
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
duration completed tasks_completed files_created files_modified
8 minutes 2026-03-24 3 8 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.pyinvitations_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.