forked from bai/curriculum-project-hub
95 lines
3.3 KiB
Markdown
95 lines
3.3 KiB
Markdown
# 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, and tenant suspension workflows are
|
|
deferred. Production safety limits and workload brakes are no longer
|
|
deferred; ADR-0022 defines them without treating a workload brake as tenant
|
|
suspension.
|
|
- 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.
|