ADR-011 (resolves OQ-08): per-repo authorization — grants live in repo records keyed on the stable logical identity id (alkcall ADR-025, referenced); policy is alkgit-core's authorize() function (public+read anonymous-first-class, write always authenticated+granted); alkgit stores no identity records; vault placement resolved as nothing to place in v1. ADR-012 (resolves OQ-06/OQ-07): registry backing + write surface — GitRegistryStore write supertrait (alknet ADR-035 read/write split shape); registry-file default (per-repo record files + in-memory index, config-seeded, op-mutable, no gitoxide); git/repo/* CRUD ops shipped External with scope+ownership ACL (create mints ownership and seeds creator grants; ownership never implies git access); the two-op-kind classification recorded (open op + call ops from one crate, per alkcall ADR-047); recorded split trigger for a downstream platform crate. Doc sync: backend.md (five-trait family, feature model split, two-op-kinds), doors.md + overview.md (authorize policy, dual-kind crate map), open-questions.md (OQ-06/07/08 resolved), README (ADR table, current state), oq-06 tracker task closed (resolved early). Verification: cargo test (default + --no-default-features), clippy -D warnings, fmt --check.
6.3 KiB
ADR-011: Per-repo authorization — grants live in repo records
Status
Accepted (resolves OQ-08)
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_idstability 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
AccessControlwith restrictions fails closed onidentity: 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
-
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-025peer_idproperty, 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. -
Grants live in repo records, keyed on the logical identity id.
RepoRecordcarriesvisibility(Public/Private) andgrants: identity_id → {read, write}. Per-repo grants are repo lifecycle data — created with the repo, administered with it — so they are stored with the record, not folded intoIdentity.resources(which is static identity-provider data resolved per credential path; per-repo entries would bend that model). -
Authorization is a policy function in alkgit core, evaluated against the record:
authorize(record, identity: Option<&Identity>, action: read|write) -> boolreadon a Public repo → allowed for anyone, includingNone(anonymous-first-class; ADR-007's operative clause).readon 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).
-
Data and policy are split across the backend seam. The
GitRegistrytrait returns the record (resolve); the policy function is alkgit-core code evaluated on it. An embedder with an existing permission system implementsresolveand 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). -
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.
-
resolveis 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.awaitthere 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 isresolve-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 + ownership, for managing records) and repo grants (for git access). Ownership never implies git access: repo creation seeds the creator's grants explicitly (ADR-012). This split is deliberate (managing vs accessing are different capabilities) but is a thing a reader must hold both of.
- Neutral: identity-id stability (key rotation) is inherited from
alkcall's
PeerEntrymodel, not guaranteed here — alkgit's contract is only "grants key on the stable logical id."
References
- OQ-08 (resolved by this ADR)
- ADR-007 (enforcement point, anonymous-public read rule), ADR-008 (registry-resolved ids), ADR-010 (trait family, no-doors rule)
- alkcall ADR-003/025 (IdentityProvider,
PeerEntrystable logical id), ADR-017 (the static ACL engine this complements) - alktty
tty:openposture (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)