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.
144 lines
7.2 KiB
Markdown
144 lines
7.2 KiB
Markdown
# ADR-011: Per-repo authorization — grants live in repo records
|
||
|
||
## Status
|
||
Accepted (resolves OQ-08; grant action set + policy domain amended by
|
||
ADR-015)
|
||
|
||
## Context
|
||
|
||
OQ-08 (narrowed by ADR-010) asked for the registry's identity model: what
|
||
an identity *is* to the `GitRegistry`, what identity records exist, where
|
||
they live, and where credential material goes.
|
||
|
||
The family precedents constrain the answer:
|
||
|
||
- alkcall resolves credentials to a stable logical `Identity`
|
||
(`IdentityProvider`, ADR-003; `PeerEntry.peer_id` stability across key
|
||
and token rotation, ADR-025). `AccessControl::check` (ADR-017) is a
|
||
flat scope/resource match against that resolved identity.
|
||
- Doors own credential presentation (alkhttp bearer → token; ssh →
|
||
fingerprint) and resolve identity before alkgit sees anything.
|
||
- alktty/alktunnels gate with a single scope constant because their
|
||
resources are session- or deployment-scoped. Git cannot use that shape:
|
||
its authorization is **per-repo**, and its primary deployment target
|
||
(vision §"Primary deployment target") makes **anonymous fetch on
|
||
explicitly-public repos first-class** — which an identity-scoped gate
|
||
cannot express, since a static `AccessControl` with restrictions fails
|
||
closed on `identity: None`.
|
||
|
||
So the git analog of `TTY_OPEN_SCOPE` is not a scope constant — it is the
|
||
per-repo check itself, at ADR-007's step-3 position. The gap is real and
|
||
known: ADR-007 already states "anonymous access on explicitly-public
|
||
repos grants the same full read path," a clause the static ACL engine
|
||
cannot express.
|
||
|
||
Two sub-questions rode along: whether identity *records* (credential
|
||
paths, scopes) live in the git metadata store, and what vault material
|
||
metadata must hold.
|
||
|
||
## Decision
|
||
|
||
1. **Identity space is alkcall's; alkgit stores no identity records.**
|
||
The subject of every authorization decision is the resolved alkcall
|
||
`Identity`, keyed by its stable logical id (`Identity.id` — the
|
||
alkcall ADR-025 `peer_id` property, referenced here, not
|
||
reimplemented). Credential material, fingerprints, and token hashes
|
||
remain in the door/assembly identity-provider seam (alkcall ADR-003,
|
||
ADR-025). alkgit's metadata store holds no identity records.
|
||
|
||
2. **Grants live in repo records, keyed on the logical identity id.**
|
||
`RepoRecord` carries `visibility` (Public/Private) and
|
||
`grants: identity_id → {read, write}` *(amended by ADR-015: the
|
||
action set gains `manage` — `grants: identity_id → {read, write,
|
||
manage}`, a flat set with no hierarchy; `manage ⊇ write ⊇ read`)*.
|
||
Per-repo grants are repo lifecycle data — created with the repo,
|
||
administered with it — so they are stored with the record, not folded
|
||
into `Identity.resources` (which is static identity-provider data
|
||
resolved per credential path; per-repo entries would bend that
|
||
model). *(Also amended by ADR-015: the grant-key id format is
|
||
deliberately an opaque string — the identity namespace is the
|
||
embedder's to define.)*
|
||
|
||
3. **Authorization is a policy function in alkgit core**, evaluated
|
||
against the record:
|
||
|
||
```
|
||
authorize(record, identity: Option<&Identity>, action: read|write) -> bool
|
||
```
|
||
|
||
*(Amended by ADR-015: the action domain extends to
|
||
`read|write|manage`; `manage` requires identity plus a `manage`
|
||
grant, and the grant check is monotonic — a `manage` grant satisfies
|
||
`write` and `read` checks. The repo-op gate "admin scope OR manage
|
||
grant" is defined there.)*
|
||
|
||
- `read` on a Public repo → allowed for anyone, including `None`
|
||
(anonymous-first-class; ADR-007's operative clause).
|
||
- `read` on a Private repo → identity required with a read grant.
|
||
- `write` → identity required **and** a write grant, on every repo
|
||
(push is always authenticated — vision §"Primary deployment
|
||
target"; ADR-007).
|
||
|
||
Enforcement timing is unchanged: resolve → authorize →
|
||
authorized-repo marker → transport (ADR-007 steps 1–4). Unknown-repo
|
||
and unauthorized remain indistinguishable (ADR-008).
|
||
|
||
4. **Data and policy are split across the backend seam.** The
|
||
`GitRegistry` trait returns the *record* (`resolve`); the policy
|
||
function is alkgit-core code evaluated on it. An embedder with an
|
||
existing permission system implements `resolve` and maps its own ACL
|
||
into grants at resolve time — no policy hook is needed, and none is
|
||
offered in v1 (a custom-policy override would be an additive default
|
||
method, a two-way door, added when an embedder needs it).
|
||
|
||
5. **Vault placement: nothing to place in v1.** With no identity records
|
||
and no credentials in metadata (AGENTS.md convention 4), alkvault
|
||
holds nothing for alkgit in v1. Vault references enter later only if
|
||
a record grows credential-shaped material (the plausible case:
|
||
mirroring/remotes records in the consumer half's orbit) — and by then
|
||
the rule is already fixed: metadata holds references, never secrets.
|
||
|
||
6. **`resolve` is async.** Unlike alkcall's accept-loop identity reads
|
||
(alknet ADR-035's sync-hot-path constraint — that trait stays sync),
|
||
the registry resolve runs once per session/request before the first
|
||
protocol byte; an `.await` there costs nothing and keeps the backend
|
||
trait family uniform (ADR-010 sub-decision 4). No cache-invalidation
|
||
machinery is load-bearing in alkgit v1 — the hot paths (pack
|
||
generation, ref listing) hit the object DB, not metadata. Backing
|
||
stores that need invalidation strategies own that inside their impl.
|
||
|
||
## Consequences
|
||
|
||
- **Positive:** anonymous-public fetch is expressible without bending
|
||
`AccessControl` (the clause ADR-007 pinned is now mechanically true);
|
||
per-repo grants are administered with the repo, in one store; policy
|
||
is one function, tested once, in core; the embedder seam for foreign
|
||
permission systems is `resolve`-time mapping (no hook, no trait
|
||
growth); no vault placement question remains for v1.
|
||
- **Negative:** two authorization data sources exist by design — op ACL
|
||
(scopes, for platform-level capability) and repo grants (for per-repo
|
||
capability). This split is deliberate but is a thing a reader must
|
||
hold both of. *(Amended by ADR-015: repo administration is the
|
||
`manage` grant — the record is the single per-repo authz surface;
|
||
"ownership never implies git access" is superseded — creation seeds
|
||
`manage`, which is a grant, not an ownership-derived bypass, and
|
||
grants are promotable via `update_repo`.)*
|
||
- **Neutral:** identity-id stability (key rotation) is inherited from
|
||
alkcall's `PeerEntry` model, not guaranteed here — alkgit's contract
|
||
is only "grants key on the stable logical id."
|
||
|
||
## References
|
||
|
||
- OQ-08 (resolved by this ADR), OQ-16 (the grant-key identity-namespace
|
||
question this ADR's opaque-string rule defers)
|
||
- ADR-007 (enforcement point, anonymous-public read rule), ADR-008
|
||
(registry-resolved ids), ADR-010 (trait family, no-doors rule)
|
||
- ADR-015 (the `manage` grant tier — amends this ADR's action set and
|
||
policy domain)
|
||
- alkcall ADR-003/025 (IdentityProvider, `PeerEntry` stable logical id),
|
||
ADR-017 (the static ACL engine this complements)
|
||
- alktty `tty:open` posture (the scope-constant pattern this ADR
|
||
deliberately does NOT reuse)
|
||
- vision.md §"Primary deployment target", §"Guiding principles" 1–2
|
||
- backend.md §"The trait family" (GitRegistry), ADR-012 (record store +
|
||
op set) |