Files
alkgit/docs/architecture/decisions/011-per-repo-authorization.md
T
glm-5.3-flash c4b9c53674 docs(architecture): ADR-015 — manage grant tier + repo-op gate, OQ-16 identity namespace
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.
2026-09-26 11:50:25 +00:00

144 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)