forked from bai/curriculum-project-hub
feat: add organization tenant model
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# ADR 0020: Organization Is The SaaS Tenant Root
|
||||
|
||||
## Status
|
||||
|
||||
Accepted.
|
||||
|
||||
## Context
|
||||
|
||||
The Hub is moving from a single-organization deployment assumption toward a
|
||||
SaaS shape. The existing permission model already supports team principals via
|
||||
`PermissionGrant(PROJECT, TEAM, role)`, but without a tenant root the data model
|
||||
cannot safely answer:
|
||||
|
||||
- which customer owns a project;
|
||||
- which organization a team belongs to;
|
||||
- whether a team grant crosses customer boundaries;
|
||||
- which external directory sync produced a Feishu department/user-group
|
||||
membership;
|
||||
- where future org admin, billing, audit, and platform break-glass controls
|
||||
attach.
|
||||
|
||||
Retrofitting this after projects, teams, runs, and audit records grow would
|
||||
force tenant assumptions into every caller.
|
||||
|
||||
## Decision
|
||||
|
||||
Introduce `Organization` as the SaaS tenant root.
|
||||
|
||||
Customer-side collaboration data is organization-scoped:
|
||||
|
||||
```text
|
||||
Organization
|
||||
OrganizationMembership
|
||||
Team
|
||||
TeamMembership
|
||||
Project
|
||||
ExternalDirectoryConnection
|
||||
PermissionGrant
|
||||
```
|
||||
|
||||
`User` remains global. A user can belong to multiple organizations through
|
||||
`OrganizationMembership`. Project-level authorization still uses
|
||||
`PermissionGrant` and `PermissionAuthorizer`; the authorizer derives the
|
||||
organization from the requested resource, then resolves teams and external
|
||||
principals in that organization scope.
|
||||
|
||||
`Team` belongs to exactly one organization. `Project` belongs to exactly one
|
||||
organization. A `PermissionGrant` that grants a `TEAM` principal on a `PROJECT`
|
||||
resource is valid only when the team and project are in the same organization.
|
||||
This invariant is pinned in `Spec.System.Organization`.
|
||||
|
||||
External directory membership is scoped through `ExternalDirectoryConnection`:
|
||||
|
||||
```text
|
||||
ExternalDirectoryConnection(
|
||||
organizationId,
|
||||
provider,
|
||||
providerTenantId,
|
||||
source
|
||||
)
|
||||
```
|
||||
|
||||
`ExternalPrincipalMembership` points at a connection rather than carrying a
|
||||
free-floating `source` string. `source` names a sync pipeline; it is not the
|
||||
tenant identity.
|
||||
|
||||
The SaaS platform control plane is separate from customer project permissions.
|
||||
`READ/EDIT/MANAGE` remains the customer-side project permission lattice.
|
||||
Platform staff access, tenant suspension, billing, and break-glass access are a
|
||||
separate control plane and must not be modeled as project `MANAGE`.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Existing single-org data is backfilled into a default organization during
|
||||
migration.
|
||||
- `Team.slug` becomes organization-scoped instead of globally unique.
|
||||
- Actor resolution is organization-aware: direct teams and synchronized external
|
||||
principals only expand inside the resource's organization.
|
||||
- Direct `USER` grants remain possible because a global user can collaborate
|
||||
across organizations when explicitly granted.
|
||||
- Future org admin UI can be built on this model without changing the
|
||||
authorization core.
|
||||
|
||||
## Open Questions / Deferred
|
||||
|
||||
- Full platform admin UI, billing, limits, and tenant suspension workflows are
|
||||
deferred.
|
||||
- Whether direct `USER` project grants should require active
|
||||
`OrganizationMembership` is deferred; explicit direct grants remain allowed
|
||||
for now.
|
||||
- Provider-specific directory semantics beyond Feishu are deferred, but the
|
||||
connection model leaves room for them.
|
||||
Reference in New Issue
Block a user