# ADR 0027 — Suite permission model: workspace-admin as implicit suite-admin; drop grantable suite-admin

- **Status:** Accepted
- **Date:** 2026-06-30
- **Deciders:** @TheurgicDuke771
- **Note:** the suite permission tiers were never formalised in an ADR — they were established directly in `suite_authz.py`; this ADR records and revises that model (so there is no prior ADR to mark `Superseded`). Folds in the earlier workspace-admin scope decisions it supersedes.
- **Related:** ADR [0010](0010-provider-agnostic-infrastructure-seams.md) (the generic `get_current_user` identity seam — Azure is one impl), [0020](0020-history-and-audit-strategy.md) (audit), [0026](0026-auth-api-keys-and-principal-seam.md) (principal/identity seam), the maintainers' compliance posture (data-access audit)

> **Amendment (2026-07-09, [ADR 0033](0033-workspace-roles-rbac.md)):** the
> workspace-admin **source** moves from the `WORKSPACE_ADMIN_EMAILS` allowlist to a
> stored `users.role` (`admin | member | viewer`); the allowlist demotes to
> bootstrap + break-glass (`is_workspace_admin` = stored role OR allowlist). This
> stales this ADR's "resolved from the allowlist at the `/me`/API layer"
> implementation line and the Related ADR-0010 "off a config allowlist" pointer.
> The implicit-suite-admin rule below is **unchanged**; the grant model gains one
> rule — no `edit` shares to Viewers (their effective level caps at `view`).

> **Amendment (2026-09-05, admin write pass):** ownership is no longer immutable. A workspace admin can transfer a suite to another member or admin (`POST /admin/suites/{id}/transfer`), revoke any per-suite share, and delete any suite; every one of these is audited with `admin_override: true`. The previous owner keeps an `edit` grant by default (clamped to `view` for a Viewer). `created_by` now means *current owner*; the creator at any past moment is recoverable from the `suite.transfer` audit trail.

> **Companion (2026-07-18, [ADR 0037](0037-workspace-visible-asset-identity.md)):** the
> suite ladder below remains the *only* grant model — but it now guards **suite-derived
> detail only** (suite/check config, runs, results, samples, incidents, and the
> composing-suite list on an asset page). Asset *identity*, lineage topology and
> **workspace-true aggregate verdicts** are visible to every workspace member and are
> no longer derived from these grants. Suite endpoints keep their 404-no-leak.

## Context

Suite authorization (`backend/app/services/suite_authz.py`) ranks four tiers —
`view < edit < admin < owner` — where a user's effective level is the highest of
**owner** (implicit: they are `suite.created_by`) or a **share** row
(`view`/`edit`/`admin`). The capability ladder:

| Tier | Adds |
|---|---|
| view | read suite, checks, results |
| edit | + create/update/delete checks, update suite, trigger runs |
| admin | + manage shares (grant/revoke) **and** delete the suite |
| owner | (identical capabilities to admin) — but the immutable creator |

Two problems with this shape:

1. **`owner` and `admin` are capability-identical.** The only difference is
   lifecycle (owner is the immutable creator; admin is a grantable/revocable
   share). Users can't tell what `admin` means versus `owner`, and the tier earns
   its keep only as a *delegation* mechanism.
2. **The delegation it enables is the wrong shape for a single-tenant product.**
   `admin` is grantable to any peer, so the most privileged capabilities
   (manage who-can-see-this, delete-the-suite) can be handed around, including an
   admin-revokes-another-admin escalation edge. Meanwhile the **workspace-admin**
   role (`WORKSPACE_ADMIN_EMAILS` → `is_workspace_admin`) is *read
   only*: it can see every suite/user/grant on `/admin` but cannot act — it can
   spot an orphaned or junk suite and do nothing about it. The product has a
   governance role that can't govern, and a per-suite admin tier that
   over-delegates.

DataQ is **single-tenant with suite-level access sharing** (CLAUDE.md §1). The
natural role model for that is *resource owner + platform admin (superuser) +
collaborators* — the shape GitHub-org and Google-Workspace use.

## Decision

Redefine the tiers so each maps to a distinct purpose, and make the
workspace-admin the governance actor instead of a grantable per-suite tier.

- **owner** — the creator; exactly one per suite; full control; immutable
  (cannot be revoked, demoted, or granted as a share).
- **admin** — **the workspace-admin(s), implicit on every suite** (computed from
  `is_workspace_admin`, never a `shares` row). Same capabilities as owner: manage
  shares, delete, edit, run, read. This is the break-glass / owner-on-leave /
  governance path.
- **edit / view** — the only permissions **grantable to normal users**. A normal
  user can no longer be made `admin` of a suite.

**Visibility (option a, decided):** workspace-admins get a **workspace-wide view
in the normal product surface** — Dashboard, Suites, Results — not just the
`/admin` page. This is consistent with being implicit admin everywhere, and it
**supersedes the earlier, narrower decisions** for workspace-wide Dashboard/Results
visibility and for workspace-admin write actions, which this decision absorbs.

Net per suite: **one owner + workspace-admin-as-admin + edit/view collaborators.**

## Consequences

**Positive**
- Roles map to purpose: creator / platform-governance / collaborator. The
  confusing redundant tier is gone.
- The governance role can finally act — delete a junk suite, re-share an orphaned
  one — without first being granted access to it.
- Collaborators are strictly least-privilege: `edit` is the ceiling; they can't
  delete the suite or change its sharing. The admin-revokes-admin escalation edge
  disappears (there are no grantable admins).

**Costs / risks (accepted)**
- **Workspace-admin becomes a write superuser over all suites** — manage shares,
  delete, edit, and **read every suite's results including failing-sample data**
  (which can be sensitive; PII redaction still applies at the logger/sample
  layer). This is a deliberate expansion from today's read-only oversight and
  wants a security note; the planned data-access audit trail should record
  workspace-admin reads. Hold the workspace-admin allowlist tightly.
- **No peer-to-peer delegation of suite management.** Management/deletion by a
  non-owner now funnels to workspace-admins. Mitigation: the allowlist already
  supports **multiple** workspace-admins — use that to avoid a bottleneck. (For
  most teams, "manage access" and "delete" are governance actions, so centralising
  them is acceptable.)
- **`owner` was immutable / non-reassignable until the admin write pass** (see the
  amendment at the top): a workspace admin can now transfer a suite to another
  member or admin. `created_by` then names the current owner; the original creator
  is preserved in the `suite.transfer` audit event, not on the row.

**Implementation shape**
- `effective_permission` / `effective_permissions` return `admin` when the user
  is a workspace-admin (computed, not a `shares` lookup).
- ~~`require_permission` takes an `is_workspace_admin` signal — today it is a pure
  `session + user_id` primitive; the flag is resolved from the allowlist at the
  `/me`/API layer and threaded in (keeps it unit-testable).~~ **Not what was
  built, and superseded by ADR 0033.** `require_permission` /
  `effective_permission` take `(session, suite, user_id)` and resolve the
  workspace role *internally*, per call, via `suite_authz._workspace_role` — so
  there is no signal for a caller to thread in, or to forget to. That also makes
  the Viewer clamp (0033) unbypassable from the route layer, which threading a
  flag would not have been.
- The share validator accepts only `view`/`edit` (rejects `admin`).
- Suite list/read scoping, Dashboard, and Results include all suites for
  workspace-admins.
- **Backward-compatible migration:** downgrade existing `shares.permission =
  'admin'` rows → `edit` (two-step deploy per the migration rule; comms to anyone
  currently holding an admin share).

## Alternatives considered

- **Keep the status quo (grantable suite-admin).** Rejected: the redundant
  owner/admin tier confuses users, over-delegates the privileged capabilities,
  and leaves the workspace-admin role unable to govern. Its one real benefit —
  peer delegation of one suite without granting global admin — is outweighed in a
  single-tenant workspace, and is recoverable via multiple workspace-admins.
- **Restrict who can *grant* admin (owner-only) + make `delete` owner-only.** A
  smaller change that fixes the escalation edge while keeping peer admin. Rejected
  in favour of the cleaner role model, but noted as the minimal fallback if the
  superuser expansion proves too broad.
- **Reserve all manage/delete to workspace-admins, drop owner control too.**
  Rejected: removes the creator's authority over their own suite and maximises the
  bottleneck.

## Related

- Supersedes the earlier decisions for workspace-wide Dashboard/Results view and
  for workspace-admin write actions from `/admin`.
- A data-access audit trail is planned — workspace-admin reads of results/samples
  should be auditable under this expanded access.
- ADR 0010 — workspace-admin is derived from the generic identity seam
  (`is_workspace_admin` ~~off a config allowlist~~ **off the stored `users.role`
  since ADR 0033; the config allowlist survives as a bootstrap seed and lockout
  break-glass that only ever grants**), not from Azure/Entra claims.
