Add enterprise-auth -revoke-membership-*/-list-memberships-tenant flags

Rounds out the tenant_memberships operator flags added earlier this
phase (-create-tenant/-grant-membership-*): grant had no way to undo
itself, and there was no way to see who was actually in a tenant
without querying Postgres directly.

rbacstore.RevokeMembership deletes a tenant_memberships row, but
refuses to revoke a tenant's current Owner -- Owner is also a
dedicated tenants.owner_user_id column (SetOwner), so deleting that
membership without transferring ownership first would leave
owner_user_id pointing at a user with no membership in the tenant at
all. Ownership transfer is a deliberate, separate action per the RBAC
matrix ("Transfer tenant Owner -- Owner only"), not a side effect of
revoking access.

rbacstore.ListMembershipsForTenant is ListMembershipsForUser's
inverse -- joined with users so the result is actually useful (email,
display name), not just a bare user ID list.

enterprise-auth gains -revoke-membership-tenant/
-revoke-membership-user-email (both required together, same shape as
-grant-membership-*) and -list-memberships-tenant (prints tab-separated
id/email/display-name/role to stdout and exits -- an operator
convenience, not a machine-readable API; this binary deliberately has
no admin HTTP surface, per its own doc comment on why). Changing a
role is unchanged: re-run -grant-membership-* with a different
-grant-membership-role, SetMembership's upsert already handles it.

Covered by five new skip-gated integration tests in rbacstore_test.go
(RBACSTORE_TEST_POSTGRES_ADDR), including the Owner-revocation refusal
and cross-tenant leak check for ListMembershipsForTenant -- not run
against a live database in this environment, same disclosed gap as the
rest of this phase's Postgres-backed work. Docs updated:
phase-4-runbook.md's known-gaps bullet, enterprise/README.md's
bootstrap walkthrough and package layout table.
This commit is contained in:
2026-08-14 09:11:01 -07:00
parent 823f5d48d1
commit cfcbc77507
5 changed files with 281 additions and 14 deletions
+8 -5
View File
@@ -557,11 +557,14 @@ Full accounting: `/docs/security/threat-model.md`. Headline items:
identity either (refused outright) for either protocol.
- No admin UI to create a `tenant_memberships` row, but §3a/§3b's manual
SQL bootstrap is gone -- `enterprise-auth -create-tenant`/
`-grant-membership-*` (offline operator flags, same shape as
`-mint-service-token`) replace it. Nothing yet for revoking a
membership, listing a tenant's members, or changing a role after the
fact (SetMembership's upsert supports it at the storage layer; there's
just no flag exposing it).
`-grant-membership-*`/`-revoke-membership-*`/`-list-memberships-tenant`
(offline operator flags, same shape as `-mint-service-token`) cover
create/grant/revoke/list. Changing a role after the fact is just
re-running `-grant-membership-*` with a different `-grant-membership-
role` (`SetMembership`'s upsert already supports it). `RevokeMembership`
refuses a tenant's current Owner (would leave `tenants.owner_user_id`
dangling) -- transferring ownership first has no flag yet, only
`rbacstore.SetOwner` at the storage layer.
- **Per-resource dashboard grants are now enforced** (`api/dashboards`'
handler reads `dashboard_permissions` via
`enterprise/internal/rbacstore.DashboardPermissions`, only when