docs: ratify ADR-017 KEEP — invite flow is load-bearing (some users can't use Google OIDC)
Records the deliberate keep-or-reverse decision on the Dex-write invite flow. Decision: KEEP. Deciding fact: not all intended Future-B users will use Google accounts, so Google OIDC alone can't onboard them — the invite flow is load-bearing, not redundant. Trust-surface cost accepted deliberately, explicitly NOT as a precedent for widening further, and explicitly NOT by adding delete RBAC to fix the orphan gap. Open items reframed as tracked follow-ups (verify RBAC against the real manifest; accept orphan for Future B, revisit before Future C). Added the reversal to rejected-alternatives.
This commit is contained in:
+31
-16
@@ -519,12 +519,25 @@ about editing a gate while it's unmet) so the reasoning survives, not just the n
|
|||||||
|
|
||||||
## ADR-017 — Invite flow: Tapir creates Dex local-password accounts (write access to the shared identity provider)
|
## ADR-017 — Invite flow: Tapir creates Dex local-password accounts (write access to the shared identity provider)
|
||||||
|
|
||||||
**Status:** Accepted (2026-06-03), **recorded retroactively during reconciliation.** This
|
**Status:** Accepted (2026-06-03), **recorded retroactively during reconciliation, then
|
||||||
capability **shipped in v0.7.0 without an ADR** — code, RBAC, and a deployed ServiceAccount
|
deliberately ratified KEEP (2026-06-03).** This capability **shipped in v0.7.0 without an ADR** —
|
||||||
landed before any decision record existed. This ADR documents what shipped and honestly records
|
code, RBAC, and a deployed ServiceAccount landed before any decision record existed. This ADR
|
||||||
that the decision-before-code discipline was not followed here (see "Process note"). **Partially
|
documents what shipped and honestly records that the decision-before-code discipline was not
|
||||||
supersedes ADR-002 and ADR-013** (the "Tapir holds no write access to the shared identity
|
followed here (see "Process note"). **Partially supersedes ADR-002 and ADR-013** (the "Tapir
|
||||||
provider" posture).
|
holds no write access to the shared identity provider" posture).
|
||||||
|
|
||||||
|
**Keep-or-reverse, decided (2026-06-03).** After the retroactive recording, the maintainer
|
||||||
|
weighed keep vs. reverse (drop the RBAC + invite flow, rely on Google OIDC only). **Decision:
|
||||||
|
KEEP.** Deciding fact: not all intended Future-B users have / will use Google accounts, so Google
|
||||||
|
OIDC alone *cannot* onboard them — the invite flow is therefore **load-bearing, not a redundant
|
||||||
|
convenience**, and reversing it would leave some intended users with no onboarding path. The
|
||||||
|
trust-surface cost (scoped `create` on `passwords` in `auth`) is accepted deliberately in
|
||||||
|
exchange. The opposing argument (this contradicts ADR-015's "no write credential to shared infra"
|
||||||
|
logic, applied there to secrets) was considered and outweighed *only* because the capability is
|
||||||
|
genuinely necessary for real users here — it is **not** a precedent for widening the surface
|
||||||
|
further. **Explicitly NOT chosen:** adding `delete` to the RBAC to close the orphan gap (that
|
||||||
|
widens the surface in the wrong direction; accept the orphan at Future B instead — see open
|
||||||
|
items).
|
||||||
|
|
||||||
**Context.** Stage 1 onboarding needs a way for a friend to get a login. Two paths shipped:
|
**Context.** Stage 1 onboarding needs a way for a friend to get a login. Two paths shipped:
|
||||||
Google OIDC via Dex (no Tapir code — Dex handles it), and an **invite flow** where Tapir itself
|
Google OIDC via Dex (no Tapir code — Dex handles it), and an **invite flow** where Tapir itself
|
||||||
@@ -556,7 +569,8 @@ the shared identity provider" posture (ADR-002/013).
|
|||||||
- The RBAC is **minimally scoped**: `create`+`get` on one resource type in one namespace, not
|
- The RBAC is **minimally scoped**: `create`+`get` on one resource type in one namespace, not
|
||||||
broad Dex/cluster write. Blast radius is bounded and inspectable.
|
broad Dex/cluster write. Blast radius is bounded and inspectable.
|
||||||
- It enables friend onboarding **without Google OAuth app verification** (ADR-008 deferred that),
|
- It enables friend onboarding **without Google OAuth app verification** (ADR-008 deferred that),
|
||||||
which is genuinely useful for Future B.
|
which is genuinely useful for Future B — and necessary for users who won't use Google (the
|
||||||
|
deciding fact in the keep decision above).
|
||||||
- The code is careful: validate-before-consume, single-use tokens, graceful off-cluster degrade,
|
- The code is careful: validate-before-consume, single-use tokens, graceful off-cluster degrade,
|
||||||
sentinel errors mapped to clear messages, the base64-bcrypt storage gotcha handled.
|
sentinel errors mapped to clear messages, the base64-bcrypt storage gotcha handled.
|
||||||
|
|
||||||
@@ -567,7 +581,8 @@ the shared identity provider" posture (ADR-002/013).
|
|||||||
- **Create/delete asymmetry → orphaned Dex accounts.** Tapir now *creates* Dex Password CRs but
|
- **Create/delete asymmetry → orphaned Dex accounts.** Tapir now *creates* Dex Password CRs but
|
||||||
(per ADR-013) does not *delete* them on account deletion. A deleted Tapir user leaves an
|
(per ADR-013) does not *delete* them on account deletion. A deleted Tapir user leaves an
|
||||||
orphaned Dex local-password account that can still authenticate (though it would hit the
|
orphaned Dex local-password account that can still authenticate (though it would hit the
|
||||||
registration gate with no profile). This widens the ADR-013 right-to-erasure gap. **Open.**
|
registration gate with no profile). This widens the ADR-013 right-to-erasure gap. **Open —
|
||||||
|
accepted for Future B, revisit before Future C (do NOT add `delete` RBAC just to fix this).**
|
||||||
- **`create` on a shared-namespace identity resource** is a meaningfully larger trust surface than
|
- **`create` on a shared-namespace identity resource** is a meaningfully larger trust surface than
|
||||||
the rest of Tapir. If the `tapir` pod is compromised, the attacker can mint Dex local accounts.
|
the rest of Tapir. If the `tapir` pod is compromised, the attacker can mint Dex local accounts.
|
||||||
Bounded by the namespace/resource scope, but real — worth a deliberate look before Future C.
|
Bounded by the namespace/resource scope, but real — worth a deliberate look before Future C.
|
||||||
@@ -579,16 +594,15 @@ parallel agent sessions while the main planning thread was elsewhere, and shippe
|
|||||||
the first time in this project a guardrail-reversing change skipped the decision-before-code
|
the first time in this project a guardrail-reversing change skipped the decision-before-code
|
||||||
discipline. Recorded here not to rubber-stamp it but to restore the audit trail: the decision is
|
discipline. Recorded here not to rubber-stamp it but to restore the audit trail: the decision is
|
||||||
now visible, its principle-reversal is named, and its open gaps (orphaned accounts, the larger
|
now visible, its principle-reversal is named, and its open gaps (orphaned accounts, the larger
|
||||||
trust surface) are tracked rather than buried in a release note. The maintainer is ratifying it
|
trust surface) are tracked rather than buried in a release note. The maintainer ratified KEEP
|
||||||
after the fact; if on reflection the trust surface is unwanted, reversing it (drop the RBAC +
|
after the fact (see status block) on the deciding fact that some intended users cannot use Google
|
||||||
invite flow, rely on Google OIDC only) is a clean follow-up ADR.
|
OIDC.
|
||||||
|
|
||||||
**Open items this ADR creates:**
|
**Open items this ADR creates (tracked in infra):**
|
||||||
- Decide whether to close the orphaned-Dex-account gap (delete the Password CR on account
|
|
||||||
deletion — but that *widens* write access to `delete`, the opposite direction; or accept the
|
|
||||||
orphan for Future B and fix at Future C).
|
|
||||||
- Confirm the RBAC really is `create`+`get` only (not broader) against the deployed manifest in
|
- Confirm the RBAC really is `create`+`get` only (not broader) against the deployed manifest in
|
||||||
`infra`.
|
`infra` — a security claim currently resting on a sibling report, not a verified manifest read.
|
||||||
|
- Accept the orphaned-Dex-account gap for Future B; revisit (orphan cleanup + the whole Dex-write
|
||||||
|
surface) before any Future C move. Do **not** add `delete` RBAC solely to fix the orphan.
|
||||||
- Review the larger trust surface before any Future C move.
|
- Review the larger trust surface before any Future C move.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -614,6 +628,7 @@ maps to the ADR that settles it.
|
|||||||
| Vault-write SA for per-user OAuth tokens (ESO as runtime write path) | ESO syncs vault→cluster at deploy time, not a runtime write API; a write-SA widens blast radius to shared infra to store app row-data | ADR-015, infra#88 |
|
| Vault-write SA for per-user OAuth tokens (ESO as runtime write path) | ESO syncs vault→cluster at deploy time, not a runtime write API; a write-SA widens blast radius to shared infra to store app row-data | ADR-015, infra#88 |
|
||||||
| Supabase for per-user credential storage | Adds a second datastore for a few encrypted strings PG18 already holds; reopens ADR-002 | ADR-015, infra#88 |
|
| Supabase for per-user credential storage | Adds a second datastore for a few encrypted strings PG18 already holds; reopens ADR-002 | ADR-015, infra#88 |
|
||||||
| Feedback-based Stage 0 gate (friends saying it's useful) | Politeness bias makes asked-for feedback the least reliable signal; return-usage is the real test | ADR-016 |
|
| Feedback-based Stage 0 gate (friends saying it's useful) | Politeness bias makes asked-for feedback the least reliable signal; return-usage is the real test | ADR-016 |
|
||||||
|
| Reverse the Dex-write invite flow (Google OIDC only) | Some intended Future-B users won't use Google; OIDC-only leaves them with no onboarding path — invite flow is load-bearing | ADR-017 |
|
||||||
|
|
||||||
If a future case genuinely reopens one of these, that's a new ADR superseding the relevant one —
|
If a future case genuinely reopens one of these, that's a new ADR superseding the relevant one —
|
||||||
not a silent reversal.
|
not a silent reversal.
|
||||||
|
|||||||
Reference in New Issue
Block a user