Resolves review 001 finding A-1 (critical): ADR-012 §3's "scope
git:admin OR ownership" gate is not expressible in alkcall's
AccessControl (AND-composition). Resolution is the review's option (a)
shape with the OR-term generalized: the per-repo grant action set gains
manage, authorize(record, identity, read|write|manage) becomes the
single policy function for git access and repo administration, and the
delete/update/get gate is admin scope OR manage grant (handler-side,
generic FORBIDDEN, unknown-repo = unauthorized per ADR-008). Repo
create seeds the creator's {read, write, manage} grants —
administration is grantable, so collaborators/bots/app-compiled roles
work without global scopes. Ownership stays as alkcall spawn-tracking
(mint at create unchanged); "ownership never implies git access" is
superseded.
- ADR-015 (new): manage grant tier, op gate, flat-grants-as-replication-
substrate, opaque grant-key rule
- ADR-011: action set + policy domain amended, references updated
- ADR-012 §3: gate table replaced, two-tier paragraph superseded
- backend.md/doors.md/overview.md: gate + grant restatements, ADR tables
- OQ-16 (new, deferred(scope)): grant-key identity namespace —
globally-comparable ids for cross-assembly/replicator grant state;
tracker task tasks/architecture/oq-16-grant-identity-namespace.md
- review 001: A-1 marked resolved (ADR-015)
- vision.md: supersession notes (Internal-ops framing, v1 grant set)
Verification: cargo test, clippy -D warnings, fmt --check, doc --no-deps
all clean.
168 lines
8.8 KiB
Markdown
168 lines
8.8 KiB
Markdown
# ADR-015: The `manage` grant tier and the repo-op gate
|
|
|
|
## Status
|
|
|
|
Accepted (resolves review 001 A-1; amends ADR-011's action set and
|
|
ADR-012 §3's gate table)
|
|
|
|
## Context
|
|
|
|
Review 001 (A-1, critical) established that ADR-012 §3's two-tier op gate
|
|
— "scope `git:admin` **or** ownership" — is not expressible in alkcall's
|
|
`AccessControl`, which composes restrictions as AND
|
|
(`required_scopes` fail hard before the ownership block ever runs;
|
|
spec.rs:94-171). The review's recommended resolution was a handler-side
|
|
check: admin scope OR `OwnershipProvider::owns(...)`.
|
|
|
|
That resolution is correct mechanically but wrong in granularity. "Owner
|
|
or admin" is the ceiling of what ownership can express, and it is the
|
|
wrong ceiling: the moment a second maintainer, a bot account, or a
|
|
collaborator needs to administer a repo they did not create, the model
|
|
has no answer short of handing out global `git:admin` (too broad) or
|
|
minting alkcall ownership records for them (wrong home — ownership is
|
|
spawn-tracking, alkcall ADR-011's mint-and-own records *who created a
|
|
resource*, not *who may administer it*). The coming platform app
|
|
(issues, PRs, per-repo admins, teams) needs per-repo administration to
|
|
be grantable, and the eventual distributed/replicator deployment needs
|
|
the same grant data to be replicable as flat state.
|
|
|
|
The mechanism to express this already exists in the corpus: ADR-011's
|
|
`authorize` policy function evaluated on the `RepoRecord` — which today
|
|
knows only `read|write`. The gap is not a missing mechanism; it is a
|
|
missing action.
|
|
|
|
## Decision
|
|
|
|
1. **The grant action set gains `manage`.** ADR-011's
|
|
`grants: identity_id → {read, write}` becomes
|
|
`grants: identity_id → {read, write, manage}` — a flat set, no
|
|
hierarchy: a grant entry is the set of actions one identity holds on
|
|
one repo, and `manage` includes read and write (a manager of the repo
|
|
can access it; `authorize` treats `manage ⊇ write ⊇ read` for the
|
|
actions below it).
|
|
|
|
2. **`authorize` is the single per-repo policy function for both git
|
|
access and repo administration.** Its action domain extends:
|
|
`authorize(record, identity: Option<&Identity>, action: read|write|manage) -> bool`.
|
|
Semantics per action:
|
|
- `read` — unchanged (ADR-011 §3: anonymous-first-class on Public).
|
|
- `write` — unchanged (identity + write-or-manage grant).
|
|
- `manage` — identity required, plus a `manage` grant. No anonymous
|
|
and no owner-bypass path: the owner administers because repo
|
|
creation *seeds their `manage` grant* (§3), not because identity
|
|
has any special shape.
|
|
- Grant-monotonicity: a `manage` grant satisfies `manage`, `write`,
|
|
and `read` checks; a `write` grant satisfies `write` and `read`;
|
|
`read` satisfies `read` only.
|
|
|
|
3. **The repo-op gate is: global admin scope OR `manage` grant.**
|
|
ADR-012 §3's gate table becomes:
|
|
|
|
| Op | Gate |
|
|
|---|---|
|
|
| `git/repo/create` | scope `git:repo:create` (static registry gate, unchanged) |
|
|
| `git/repo/delete` | scope `git:admin` **or** `manage` grant (handler-evaluated) |
|
|
| `git/repo/update` | scope `git:admin` **or** `manage` grant (handler-evaluated) |
|
|
| `git/repo/get` (list/read) | scope `git:admin` **or** `manage` grant (handler-evaluated) |
|
|
|
|
Mechanically this is review 001's option (a) with the OR-term
|
|
generalized from ownership to the `manage` grant: `create` keeps its
|
|
pure-scope `AccessControl` (`required_scopes: ["git:repo:create"]` —
|
|
the static engine serves it, AND-composition is fine with one
|
|
restriction); `delete`/`update`/`get` carry an empty
|
|
`AccessControl::default()` and the handler evaluates the two-tier
|
|
rule as one tested function in alkgit core (admin scope in
|
|
`Identity.scopes` OR `authorize(record, identity, manage)`), denying
|
|
with the generic FORBIDDEN. The ADR-008 rule is preserved at the op
|
|
layer: unknown-repo and not-authorized collapse to the same denial —
|
|
no existence oracle.
|
|
|
|
4. **Repo creation seeds the creator's `manage` grant.** `git/repo/create`
|
|
mints alkcall ownership (alkcall ADR-011, `OwnershipStore::record` —
|
|
unchanged; ownership remains the platform's spawn-tracking record)
|
|
**and** writes the creator's grants as the full set `{read, write,
|
|
manage}` in the `RepoRecord`. "Creator administers their own repos"
|
|
becomes a grant, administered with the repo: the creator can
|
|
`update_repo` to promote a collaborator to `manage` or demote
|
|
themselves, using the op that already exists (`update_repo`), with no
|
|
new op and no trait change.
|
|
|
|
5. **alkcall's `AccessControl` is not extended.** The scope-OR-grant
|
|
composition lives handler-side, which is exactly where ADR-011 §4
|
|
put policy ("data and policy are split across the backend seam" —
|
|
data in the backend, policy in core). This sidesteps
|
|
`AccessControl`'s AND-composition rather than changing it: an
|
|
upstream family-crate change (review 001's option (c)) is not
|
|
required, and the two-tier rule stays unit-testable in-crate instead
|
|
of hiding in an assembly-supplied `OwnershipProvider` overload
|
|
(option (b)'s rejected shape).
|
|
|
|
6. **Ownership no longer implies administration; grants do.** This
|
|
supersedes the ADR-011/ADR-012 sentence "ownership never implies git
|
|
access" — the two-data-sources rule is *retired*, not extended: repo
|
|
records now carry the grant state both access tiers evaluate. The
|
|
op ACL (scopes) and the record grants remain distinct *sources* —
|
|
scopes govern platform-level capability, grants govern per-repo
|
|
capability — but the record is now the single per-repo surface. The
|
|
`manage` grant is the app's promotion path today (the platform app
|
|
compiles roles/teams into flat grants and writes them with
|
|
`update_repo`); nothing about roles, teams, or orgs enters the
|
|
protocol crate — ADR-012 §5's split trigger stands: that is
|
|
downstream's crate, compiling down to these flat grants.
|
|
|
|
7. **Grant keys are opaque strings; the id namespace is deliberately
|
|
open.** Grants key on the resolved identity's stable logical id
|
|
(ADR-011 §1), which is alkcall-cluster-local today. The id format is
|
|
deliberately unspecified at the alkgit layer — an opaque string — so
|
|
a downstream identity space (e.g. a `user/repo`-shaped, or
|
|
contract-derived, globally-comparable id) can be introduced by the
|
|
assembly/identity-provider seam without an alkgit change. Two
|
|
identities colliding on one string is the embedder's identity
|
|
guarantee to make, not alkgit's.
|
|
|
|
8. **Flat grants are the replication substrate.** Grants being flat
|
|
sets keyed on opaque id strings, carried in the record, and mutated
|
|
only through the store's `update_repo` is precisely the shape a
|
|
future replicator/hub needs: an external authority (the planned
|
|
contract watcher) can hold and publish grant state, and the local
|
|
store consumes it as records. Nothing here builds replication — but
|
|
the data model is chosen so that the external-authority case is
|
|
`update_repo` calls from a sync writer, not a schema change.
|
|
|
|
## Consequences
|
|
|
|
- **Positive:** A-1 resolves without upstream changes; per-repo
|
|
administration becomes grantable (the collaborator/admin case is now
|
|
expressible); one policy function serves git access and repo
|
|
management; the eventual app's roles/teams compile to flat grants
|
|
with no protocol-crate growth; the record is the single per-repo authz
|
|
surface a replicator would mirror.
|
|
- **Negative:** the grant set widens from two to three actions — a
|
|
wire-schema decision that enters OQ-03's freeze inventory (N-3's
|
|
schema pinning must land *after* this, so the freeze captures the
|
|
three-action shape once); the two-data-sources consequence recorded in
|
|
ADR-011 is partially retired, so readers of that ADR must follow the
|
|
amendment (recorded there).
|
|
- **Neutral:** ownership records and grants coexist with distinct
|
|
purposes (platform spawn-tracking vs per-repo capability); nothing in
|
|
v1 reads ownership for authorization except the create-op's mint, so
|
|
the mint could even become optional in a downstream that doesn't want
|
|
spawn-tracking — kept in v1 because `git/repo/*` ops are family-shared
|
|
and the record is cheap.
|
|
|
|
## References
|
|
|
|
- Review 001 A-1 (the trigger; this ADR takes its option (a) shape with
|
|
the OR-term generalized), N-3 (the schema-pinning rider this ADR must
|
|
precede)
|
|
- ADR-011 (the `authorize` policy function and grant storage this ADR
|
|
amends), ADR-012 §3 (the gate table this ADR replaces), ADR-012 §5
|
|
(the platform-crate split trigger this ADR preserves)
|
|
- ADR-007 (enforcement order), ADR-008 (unknown ≡ unauthorized at the
|
|
op layer), ADR-010 (trait family, no-doors)
|
|
- alkcall ADR-011 (ownership mint-and-own — retained as spawn-tracking),
|
|
`AccessControl` (spec.rs:94 — the AND-composition this ADR routes
|
|
around, not through)
|
|
- vision.md §"Primary deployment target" (the grant set remains
|
|
simple enough to reason about completely)
|
|
- backend.md §"The two op kinds" (op-gate restatement) |