Beta review: does live group resolution hold up for peering allow-lists? #125

Closed
opened 2026-08-05 18:59:01 +00:00 by Cordy · 5 comments
Owner

Scheduled revisit of a v0.5 decision, to be judged against real beta usage rather than reasoning. Full argument in #123 (comment of 2026-08-05).

What was decided: peering senders and receivers allow-lists store the group name and resolve membership live per transfer (cached, with a last-known-good fallback on IdP outage). Cairn does not expand a group into a stored user list.

The alternative that was declined: materialize group membership at assignment time, tracking whether each user was granted via group or directly, so group removal revokes only what it granted, direct grants survive, and a user in two listed groups keeps access when one is removed. That provenance design is correct and is written out in full on #123 — it was declined because all three of those problems are created by materializing in the first place, it conflicts with "identity is consumed, not implemented", it reopens the revocation window #96 closed, and it makes the stored config record a computation instead of an admin's intent.

What beta should actually test

  1. Latency. Does live resolution add noticeable delay to PrepareTransfer? It sits in the inbound path before any bytes flow.
  2. Revocation in practice. Remove someone from a listed group in Keycloak; confirm inbound transfers to them start failing within the cache TTL, not at next login.
  3. IdP outage. Stop Keycloak mid-transfer and confirm the last-known-good fallback behaves as intended — and that "never resolved" fails closed rather than open.
  4. The no-admin-client case. On an instance without the Keycloak admin client, a group-only receivers list matches nobody. Correctly fail-closed, but silently useless — does an admin actually hit this, and is the UI warning enough? (See #123.)
  5. Does anyone want offline operation? Materialization becomes the right answer if peering must work with the IdP unreachable for long stretches. That is the stated condition for reversing the decision.

Close this with either "live resolution confirmed" or a link to the issue implementing the materialized design.

Scheduled revisit of a v0.5 decision, to be judged against real beta usage rather than reasoning. Full argument in #123 (comment of 2026-08-05). **What was decided:** peering `senders` and `receivers` allow-lists store the **group name** and resolve membership live per transfer (cached, with a last-known-good fallback on IdP outage). Cairn does **not** expand a group into a stored user list. **The alternative that was declined:** materialize group membership at assignment time, tracking whether each user was granted via group or directly, so group removal revokes only what it granted, direct grants survive, and a user in two listed groups keeps access when one is removed. That provenance design is correct and is written out in full on #123 — it was declined because all three of those problems are created by materializing in the first place, it conflicts with "identity is consumed, not implemented", it reopens the revocation window #96 closed, and it makes the stored config record a computation instead of an admin's intent. ## What beta should actually test 1. **Latency.** Does live resolution add noticeable delay to `PrepareTransfer`? It sits in the inbound path before any bytes flow. 2. **Revocation in practice.** Remove someone from a listed group in Keycloak; confirm inbound transfers to them start failing within the cache TTL, not at next login. 3. **IdP outage.** Stop Keycloak mid-transfer and confirm the last-known-good fallback behaves as intended — and that "never resolved" fails closed rather than open. 4. **The no-admin-client case.** On an instance without the Keycloak admin client, a group-only `receivers` list matches nobody. Correctly fail-closed, but silently useless — does an admin actually hit this, and is the UI warning enough? (See #123.) 5. **Does anyone want offline operation?** Materialization becomes the right answer if peering must work with the IdP unreachable for long stretches. That is the stated condition for reversing the decision. Close this with either "live resolution confirmed" or a link to the issue implementing the materialized design.
Author
Owner

Triage: tandem (beta judgement). Decide, from real use, whether live group resolution (vs materialising IdP group membership) holds up for peering allow-lists. Feeds the #150/#176 design. No build until the call is made.

**Triage: tandem (beta judgement).** Decide, from real use, whether live group resolution (vs materialising IdP group membership) holds up for peering allow-lists. Feeds the #150/#176 design. No build until the call is made.
Author
Owner

after #211 epic, members are encrypted stored in .cairn-state/ at least for the ones who ever were assigned to a Spaces folder.
this can be potentially used to share available users via peering for easiers knowledge what potential "receivers" are at another instance. so this would Improve UI/UX for users using this feature, while blast radius is already kept at a minimum.
need to investigate if the shared keys for instance peering could be used to decrypt.
OR
another member list gets created and stored in the backend an be able to be decrypted with the shared key between the instances. this member list is already created on a per peering instance card as "can receive" so when peering this list could be exchanged, and upon adding or removing members, this can be re-read when someone prepares a file to be transfered, and the "receiver" list gets populated by the information from the to be sent to peer by a call checking their "can receive" member list.

after #211 epic, members are encrypted stored in .cairn-state/ at least for the ones who ever were assigned to a Spaces folder. this can be potentially used to share available users via peering for easiers knowledge what potential "receivers" are at another instance. so this would Improve UI/UX for users using this feature, while blast radius is already kept at a minimum. need to investigate if the shared keys for instance peering could be used to decrypt. OR another member list gets created and stored in the backend an be able to be decrypted with the shared key between the instances. this member list is already created on a per peering instance card as "can receive" so when peering this list could be exchanged, and upon adding or removing members, this can be re-read when someone prepares a file to be transfered, and the "receiver" list gets populated by the information from the to be sent to peer by a call checking their "can receive" member list.
Author
Owner

Decision (2026-08-14): live resolution confirmed — and extended to recipient discovery. Greenlit.

The beta-review question is settled in favour of keeping live group resolution; materialization stays rejected. Beyond confirming the authorization model, this issue now tracks the concrete feature the review surfaced: recipient discovery for peering sends (the sender getting a picker of who it can send to at a peer, instead of typing blind).

Chosen design — Option 1: live ListRecipients RPC. One method on the existing authenticated control plane (Hello → Authenticate, same session PrepareTransfer uses). The receiver computes, live, exactly the set it would accept: Receivers allow-list ∩ resident users (group receivers resolved via #96, empty without the admin client). The sender renders it as the #226-style picker.

  • Reuses the exact authorization boundary that already gates delivery — no new key, no new trust.
  • Leaks nothing new: §4.3 already accepts that an authenticated peer can enumerate recipients via probes. This turns many silent probes into one audited call (peer-recipient-list).
  • Reinforces the live decision rather than reversing it: revocation is instant at next compose.

Why not materialize (Options 2/3, the #125 comment's push/decrypt ideas): the offline argument that justifies caching for authorization has no analogue for discovery — you can only discover recipients at a peer you can reach, and you can only send to a peer you can reach. Reusing the app-owned spacestore is also the wrong data set (Space members ≠ eligible receivers), and its "decrypt with the shared peering key" premise breaks minimal-blast-radius (the peering key is an Ed25519 auth credential, not a state-decryption key).

Degraded fallback (Option 4), leak-free: when the peer can't enumerate (group receivers, no admin client) or is unreachable, the field degrades to free-text seeded from a local datalist of the sending user's own previously-used recipients for this peer — that data already lives in peer-transfers.json (Outbound.Recipient), so it's the sender's own history, never the peer's roster. Offline needs no special path: the send fails with a visible failed transfer + reason, so the user always gets an output.

Full analysis + ranking table in the decision memo. Sequenced behind #176#150 (the card is where the picker lives).

**Decision (2026-08-14): live resolution confirmed — and extended to recipient discovery.** Greenlit. The beta-review question is settled in favour of **keeping live group resolution**; materialization stays rejected. Beyond confirming the authorization model, this issue now tracks the concrete feature the review surfaced: **recipient discovery for peering sends** (the sender getting a picker of who it can send to at a peer, instead of typing blind). **Chosen design — Option 1: live `ListRecipients` RPC.** One method on the existing authenticated control plane (`Hello → Authenticate`, same session `PrepareTransfer` uses). The **receiver** computes, live, exactly the set it would accept: `Receivers` allow-list ∩ resident users (group receivers resolved via #96, empty without the admin client). The sender renders it as the #226-style picker. - Reuses the exact authorization boundary that already gates delivery — no new key, no new trust. - Leaks nothing new: §4.3 already accepts that an authenticated peer can enumerate recipients via probes. This turns many silent probes into **one audited call** (`peer-recipient-list`). - Reinforces the live decision rather than reversing it: revocation is instant at next compose. **Why not materialize (Options 2/3, the #125 comment's push/decrypt ideas):** the offline argument that justifies caching for *authorization* has no analogue for *discovery* — you can only discover recipients at a peer you can reach, and you can only send to a peer you can reach. Reusing the app-owned spacestore is also the wrong data set (Space members ≠ eligible receivers), and its "decrypt with the shared peering key" premise breaks minimal-blast-radius (the peering key is an Ed25519 auth credential, not a state-decryption key). **Degraded fallback (Option 4), leak-free:** when the peer can't enumerate (group receivers, no admin client) or is unreachable, the field degrades to free-text seeded from a **local** datalist of the sending user's *own previously-used recipients for this peer* — that data already lives in `peer-transfers.json` (`Outbound.Recipient`), so it's the sender's own history, never the peer's roster. Offline needs no special path: the send fails with a visible `failed` transfer + reason, so the user always gets an output. Full analysis + ranking table in the decision memo. Sequenced behind #176 → #150 (the card is where the picker lives).
Cordy closed this issue 2026-08-14 02:49:58 +00:00
Author
Owner

Shipped in v0.6.22 (PR #233, squash-merged; CI green in 41s; protobuf regenerated and full go build/vet/test green on the runner before merge). This is the concrete build the recipient-discovery decision produced — Option 1 (live ListRecipients) + Option 4 (validated free-text) as the leak-free fallback.

What landed:

  • ListRecipients RPC on the authenticated cairn.peer.v1.Peering control plane. The receiver returns its Receivers user allow-list narrowed to residents, reusing the exact delivery gate (Recipients.Exists, #123), as one audited peer-recipient-list event. It moves no data and returns only names an authenticated peer could already confirm one at a time via size-0 PrepareTransfer probes (§4.3) — so it formalizes that enumeration rather than widening it. Fail-closed on a disabled/paused peer.
  • Group receivers are not expanded (no IdP enumeration): a group-only receivers list yields no suggestions and the field stays free-text — still succeeds at send time via the live group check. Matches the #226 silent-degrade decision; no group names leak.
  • GET /api/v1/peering/peers/{peer}/recipients, gated exactly like the peers list (only a permitted sender may enumerate; a peer the caller may not use is 404, indistinguishable from absent). {recipients, live:true} on success.
  • Leak-free fallback: when the peer is unreachable or can't enumerate, the picker is seeded from the caller's OWN previously-used recipients for that peer ({live:false}) — local send history from peer-transfers.json, never the peer's roster. An offline peer can't be sent to anyway; the async pipeline still surfaces that as a visible failed transfer, so the user always gets an output.
  • Send dialog gains a <datalist> picker populated on open and on peer change, with a hint distinguishing live vs. history. Recipient field stays free-text.

Tests: endpoint (live / history-fallback / fail-closed-for-non-senders / anonymous), ListRecipients (resident-allow-listed / disabled-peer), ReceiverUsers.

Deployed to both dogfoods, confirmed live on digest sha256:c720da67… (the published v0.6.22): cairn-enc (IdP-managed, .249) and cairn-openbao (app-owned, .247), both Running, no restarts. Visual click-through on files.c0rdyceps.ch is the remaining human step (redeploy logged sessions out; I can't log in). Closing on shipped + tested + deployed; reopen if the picker misbehaves in the browser.

The v0.5 peering trio (#176 backend, #150 card UI, #125 discovery) is complete.

Shipped in **v0.6.22** (PR #233, squash-merged; CI green in 41s; protobuf regenerated and full `go build`/`vet`/`test` green on the runner before merge). This is the concrete build the recipient-discovery decision produced — Option 1 (live `ListRecipients`) + Option 4 (validated free-text) as the leak-free fallback. What landed: - **`ListRecipients` RPC** on the authenticated `cairn.peer.v1.Peering` control plane. The receiver returns its `Receivers` user allow-list narrowed to residents, reusing the *exact* delivery gate (`Recipients.Exists`, #123), as one audited `peer-recipient-list` event. It moves no data and returns only names an authenticated peer could already confirm one at a time via size-0 `PrepareTransfer` probes (§4.3) — so it formalizes that enumeration rather than widening it. Fail-closed on a disabled/paused peer. - **Group receivers are not expanded** (no IdP enumeration): a group-only receivers list yields no suggestions and the field stays free-text — still succeeds at send time via the live group check. Matches the #226 silent-degrade decision; no group names leak. - **`GET /api/v1/peering/peers/{peer}/recipients`**, gated exactly like the peers list (only a permitted sender may enumerate; a peer the caller may not use is 404, indistinguishable from absent). `{recipients, live:true}` on success. - **Leak-free fallback:** when the peer is unreachable or can't enumerate, the picker is seeded from the caller's OWN previously-used recipients for that peer (`{live:false}`) — local send history from `peer-transfers.json`, never the peer's roster. An offline peer can't be sent to anyway; the async pipeline still surfaces that as a visible failed transfer, so the user always gets an output. - **Send dialog** gains a `<datalist>` picker populated on open and on peer change, with a hint distinguishing live vs. history. Recipient field stays free-text. Tests: endpoint (live / history-fallback / fail-closed-for-non-senders / anonymous), `ListRecipients` (resident-allow-listed / disabled-peer), `ReceiverUsers`. Deployed to both dogfoods, confirmed live on digest `sha256:c720da67…` (the published v0.6.22): `cairn-enc` (IdP-managed, .249) and `cairn-openbao` (app-owned, .247), both Running, no restarts. Visual click-through on files.c0rdyceps.ch is the remaining human step (redeploy logged sessions out; I can't log in). Closing on shipped + tested + deployed; reopen if the picker misbehaves in the browser. The v0.5 peering trio (#176 backend, #150 card UI, #125 discovery) is complete.
Author
Owner

To close the loop on the issue as originally written (the beta-review of live group resolution), separate from the recipient-discovery feature above:

Decision on the fork: live resolution confirmed. The recipient-discovery decision memo (2026-08-14) re-examined live-vs-materialize and reaffirmed live: config stays intent, revocation is instant, and materialization is declined for the same reasons #123 declined it. ListRecipients (this release) is the live model extended to discovery, not a reason to abandon it — which is corroborating evidence for the live direction rather than against it. The one condition named for reversing (peering must work with the IdP unreachable for long stretches) has no analogue for discovery and did not materialize as a real requirement.

The concrete beta checks stay live work, folded into the v0.7 closed beta (this issue's milestone), not dropped:

  1. Latency of live resolution in PrepareTransfer — measure under real transfers.
  2. Revocation timing — remove from a listed group, confirm inbound fails within the cache TTL.
  3. IdP-outage last-known-good vs. fail-closed-on-never-resolved.
  4. The no-admin-client group-only case (silently-useless) — is the UI warning enough? (#129 covers the admin-warning surface.)

Closing as live resolution confirmed; if real beta usage surfaces a case for materialization, that reopens as a fresh design issue rather than reverting this one. #129 remains open for the group-only-receivers admin warning.

To close the loop on the issue *as originally written* (the beta-review of live group resolution), separate from the recipient-discovery feature above: **Decision on the fork: live resolution confirmed.** The recipient-discovery decision memo (2026-08-14) re-examined live-vs-materialize and reaffirmed live: config stays intent, revocation is instant, and materialization is declined for the same reasons #123 declined it. `ListRecipients` (this release) is the live model *extended to discovery*, not a reason to abandon it — which is corroborating evidence for the live direction rather than against it. The one condition named for reversing (peering must work with the IdP unreachable for long stretches) has no analogue for discovery and did not materialize as a real requirement. **The concrete beta checks stay live work, folded into the v0.7 closed beta** (this issue's milestone), not dropped: 1. Latency of live resolution in `PrepareTransfer` — measure under real transfers. 2. Revocation timing — remove from a listed group, confirm inbound fails within the cache TTL. 3. IdP-outage last-known-good vs. fail-closed-on-never-resolved. 4. The no-admin-client group-only case (silently-useless) — is the UI warning enough? (#129 covers the admin-warning surface.) Closing as **live resolution confirmed**; if real beta usage surfaces a case for materialization, that reopens as a fresh design issue rather than reverting this one. #129 remains open for the group-only-receivers admin warning.
Sign in to join this conversation.
No labels
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#125
No description provided.