Skip to content

Epic: configurable Projects + per-tenant isolation + authz-core fixes #26

Description

@iarunsaragadam

Epic: configurable Projects + per-tenant isolation, and the authz-core fixes

Audit of whether this service can back (a) a kids learning platform, (b) a working-professionals learning platform (launching this week), and (c) many SaaS integrations (email, Slack, Linear, incident.io, docs, slides). It can back the existing B2C/B2B membership surface today, but falls short of the roadmap. This epic tracks the foundational slice being implemented now; the rest are filed as #8#25.

Verdict

The repo is a clean, correct Zanzibar-style authz core (union/computed/tuple_to_userset over project-sharded relation tuples, with memory+postgres drivers behind a conformance suite). For the existing B2C/B2B membership surface it is sufficient and ships safely today: workspaces, members, groups, invitations, and concrete-user Check/Expand all work and are data-isolated by project_id. But it is NOT sufficient for the broader product roadmap, and it has one genuine correctness/security defect that the pro platform must not ship around: SUSPENDED membership is cosmetic — a suspended member still passes every Check because the engine is strictly monotonic (no exclusion operator) and Check never consults membership status, so there is no safe way to revoke a member's live access without destroying their membership. That, plus the absence of any safe bulk deprovisioning path, is the only true P0 for a this-week pro launch.

Everything else is real but additive and sequenceable. The findings collapse into a small number of foundational engine/schema capabilities — a per-project configurable model + schema-write API, the intersection/exclusion rewrite operators, a wildcard/public subject, a condition/caveat (ABAC) layer, tuple TTL, userset-subject Check, ListObjects/BatchCheck, a consistency token, and an append-only audit/history layer — on top of which the product- and compliance-facing needs (kids COPPA consent/age-gating/erasure, docs link-sharing and nested folders, pro seats/roles/cohorts, integrations scoped delegation/principal mapping, residency, metrics, rate limiting) all build. I merged the ~40 raw findings into 23 issues: the same exclusion operator surfaced in 5 findings (engine-completeness, suspended members, kids restricted accounts, kids consent-withdrawal, docs "except Bob") becomes one engine issue plus consumers; wildcard surfaced in 5; conditions/caveats in 6; TTL in 5; audit in 4; ListObjects in 4; per-project model in 5. All changes are wire-additive (new proto field numbers, new RPCs, new optional config, NULL-defaulted columns, new namespaces), default behavior is preserved when new fields are unset, and existing stored tuples and callers are unaffected — consistent with the user's explicit backward-compat mandate and the repo's no-shims rule (we change the shape additively, not via translation layers).

Shipping now — branch feat/configurable-projects-tenancy

Identity parity (per identity ADR-0001/0002) plus the keystone engine fixes. All additive; existing callers and stored tuples are unaffected, default behavior preserved.

  • First-class, configurable ProjectProject{id,name,status,config_json,...}, Admin{Create,Get,Update,List}Project RPCs gated by GATEWAY_ADMIN_API_SECRET, idempotent ensureDefaultProject bootstrap. (was #per-project-configurable-model)
  • Per-project authorization model — model stored in project config, resolved per request with an in-process cache, falling back to DefaultModel() when unset (so every existing project behaves exactly as today). Lifts the single hardcoded DefaultModel.
  • Clean per-tenant data separationtenant_id threaded alongside project_id through proto, both drivers, the engine reader, the service layer, and the conformance suite (new TenantIsolation test). Empty tenant_id ⇒ default tenant "" (no backfill, no behavior change).
  • Intersection + exclusion rewrite operators — root-cause fix for cosmetic SUSPENDED, "shared with team except Bob", consent-withdrawal denial, restricted accounts. (was #engine-intersection-exclusion-operators)
  • Wildcard/public subject (user:*) — link-sharing / published-course visibility. (was #wildcard-public-subject)
  • ListObjects reverse query — "what can this learner see?" (correctness-first; reverse-index perf is a follow-up). (was #listobjects-reverse-index)
  • P0 — real suspend/deprovisionSuspendMember/ReinstateMember (deny by tuple absence) + atomic userset-aware DeprovisionUser. (was #safe-suspend-deprovision)

Deferred — filed as backward-compatible issues

Attribute & time aware: #8 conditions/caveats (ABAC: parental consent, age-gating, scoped delegation) · #9 tuple expiry/TTL
Subject kinds & scale: #10 userset-subject Check · #11 BatchCheck
Accountability (SOC2/COPPA/GDPR): #12 tuple-change history · #13 decision/audit log · #14 subject erasure & export
Consistency: #15 zookie/consistency token
Pro platform: #16 seat/license enforcement · #17 product-defined domain roles (instructor/learner/TA) · #18 cohort/enrollment lifecycle · #22 ownership transfer
Integrations: #19 scoped delegation / on-behalf-of · #20 service-credential→principal mapping
Docs/Slides: #21 nested-folder inheritance
Operability: #23 decision metrics · #24 per-tenant rate limiting · #25 data-residency routing

Backward-compatibility mandate

Every change here and in #8#25 is additive (new proto field numbers, new RPCs, new optional config, NULL/empty-defaulted columns, new namespaces). No shims or translation layers (per AGENTS.md) — shapes change additively. Existing stored tuples and existing callers keep working with unchanged defaults.

Metadata

Metadata

Assignees

No one assigned

    Labels

    authz-coreCore ReBAC engine / modelepicTracking / umbrella issuelaunch-blockerBlocks this-week launch

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions