Files
alkgit/docs/architecture/decisions/011-per-repo-authorization.md
T
glm-5.3-flash addc874667 docs(architecture): resolve OQ-06/07/08 — per-repo authz, registry backing, CRUD ops
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.
2026-09-21 16:26:59 +00:00

6.3 KiB
Raw Blame History

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_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}. 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).

  3. Authorization is a policy function in alkgit core, evaluated against the record:

    authorize(record, identity: Option<&Identity>, action: read|write) -> bool
    
    • 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 + 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 PeerEntry model, 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, 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)