Peering recipient picker: should group receivers expand to member names? (deep-dive) #234
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: Cordy/Cairn#234
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Deep-research / deep-dive request. Evaluate permanent, sound approaches and give a ranked recommendation. Weight stability, security, long-term maintainability, and UX. Explicitly do not down-weight an option for being harder to implement — implementation cost is not an argument against the correct design.
What surfaced
During the 2026-08-14 A↔B peering dogfood (files/enc ↔ files-bao/openbao), the receivers allow-list on a peer was set to the
cairn-adminsgroup only (the explicit user was deliberately removed to test the group path). Result: the recipient picker (#125) rendered empty and degraded to free-text — even on files/enc, which has the Keycloak admin client.Root cause is by-design in the current build:
ListRecipients→Registry.ReceiverUsers(peer)returns only the explicitReceivers.Users, and never expandsReceivers.Groupsinto member usernames. So a group-only receivers list yields no suggestions anywhere.The divergence
This contradicts the wording of the recipient-discovery decision memo, Option 1: "Receivers allow-list ∩ resident users (group receivers resolved via #96; empty without the admin client)." The memo implied group receivers would be expanded where an enumeration source exists; the shipped implementation never expands them. The build-time note ("degrade silently, groups not expanded") quietly overrode the memo without recording a decision. That gap is what this issue exists to resolve properly.
Important scoping — authorization is NOT affected
To be explicit: real transfers to a group member already work. On
PrepareTransfer,Recipients.Existsresolves the recipient's groups live (#96) andCanReceivehonoursReceivers.Groups. So a send to acairn-adminsmember is authorized and delivered even with a group-only list. This issue is purely about the discovery / autocomplete UX: whether the picker should reflect group membership, not whether group-based authorization works.The question
Should the picker expand group receivers into member usernames when the instance can enumerate them — and if so, how, permanently and soundly?
Dimensions to evaluate
PrepareTransferprobes (the §4.3 oracle #125 already formalizes); #125 already returns explicit resident users. Does group expansion cross a genuinely new line, or is it the same set by another path? Decide with the threat model, not vibes.livegroups) is user→groups, not group→members — it cannot expand a group. Group→members needs either the Keycloak admin "get group members" API or the #226 candidates enumeration (Manager.Users/view-users). Which source; is it correct (not just "users ever seen"); and what is its availability across custody modes?Deliverable
Ranked options (e.g. never-expand / live-expand-when-admin-client / hint-only / hybrid) with a clear recommendation and the decision recorded, in the style of the recipient-discovery memo. Then a follow-up implementation issue if a change is chosen.
Refs: #125, #123, #96, #226; decision memo
cairn-peering-recipient-discovery-decision.Deep-dive: should the peering recipient picker expand group receivers?
Decision memo, style of
cairn-peering-recipient-discovery-decision. Weighting per the request: security, stability, long-term maintainability, UX. Implementation cost is not a factor.1. The truth in the code (verified against
main)Service.ListRecipientsiteratesRegistry.ReceiverUsers(peer)(explicitReceivers.Usersonly) and keeps eachuwhereRecipients.Exists(peer, u)(allow-listed and resident, the #123 delivery gate). Groups are never touched. Onepeer-recipient-listaudit event per call.PrepareTransfer → Recipients.Exists → CanReceive(peer, username, groups)resolves the recipient's groups live (#96, user→groups) and honoursReceivers.Groups. A send to acairn-adminsmember is authorized and delivered today with a group-only list. This issue changes no authorization behaviour.2. The enumeration-source constraint (this decides most of it)
Expanding a group needs group→members. The sources:
livegroupsManager.Users/ view-users)GET …/groups/{id}/members)query-groups/view-users)member/memberOfCorrectness landmine to record: #226's
Manager.Usersis the whole realm, not a group's members. Using it to "expand" a group would suggest non-members and users who'd be refused at delivery. Expansion must use a real group→members query, and must still be ∩ residents ∩CanReceive, so the picker never suggests someone the delivery gate would reject.3. The privacy line — does expansion cross a new one? (threat model, not vibes)
The issue's three counterweights, weighed:
Receiversallow-list lives on the receiver's peer entry; the group names are the receiver's taxonomy. The sender does not necessarily know them. Expansion discloses receiver-side group membership and, for the hint variant, receiver-side group names, to the sender.PrepareTransferoracle." — True but materially weaker than a dump. The oracle is guess-and-confirm: it validates a username the sender already suspected. Expansion is bulk disclosure: it hands over the membership list with no prior knowledge. Converting a bounded confirm-what-you-know oracle into an unbounded enumerate-from-nothing list is a real escalation, not the same set by another path.cairn-adminsmay itself be sensitive.Conclusion: expansion is a genuine, if modest, escalation in disclosure. Industry posture agrees — group→member enumeration to an external party is a recognized directory-enumeration risk, gated behind trust + audit (which the peering plane already has: mutual key auth, admin-established peering, per-call audit). It is a legitimate policy choice, but because it discloses the receiver's data to a third party, the choice belongs to the receiver admin, and the safe default is off.
4. Industry comparison
group@domain— the sender never expands it; the group is an opaque handle the remote server resolves internally at delivery. Enumeration is sidestepped entirely because the group is the address. Cairn can't copy this directly: delivery is per-username into/home/<recipient>/Inbox/<peer>/, so a group is not an addressable target today. Making it one (send-to-group fan-out) is a larger architectural change and is out of scope here — but it is the cleanest long-term answer to "how do I send to a team," and should be its own parked issue.5. Live vs materialized
Discovery must stay live (a group→members query at
ListRecipientstime), never a stored member list. A cached materialization reintroduces exactly the staleness/revocation window that the liveCanReceivedecision (#123/#125) closed. Non-negotiable: if we expand, we expand live, per send-dialog open.6. Ranked options
A — Never expand (status quo, made a recorded decision). Picker = explicit resident users only; group-only → empty → free-text; authz still live. Zero new disclosure, one contract everywhere, matches Nextcloud's no-sender-expansion posture. Costs the group-only UX.
B — Always live-expand where a source exists. keycloak-profile/LDAP expand via the real group→members query ∩ residents ∩
CanReceive; openbao/app-owned degrade. Best UX where possible, stays live, contained to the receiver. But makes bulk group-membership disclosure automatic and non-consensual for the receiver admin.C — Group-name hint only. No members; where
Receivers.Groupsis non-empty, show a non-selectable hint ("also: members ofcairn-admins— ask them for their username"). Some guidance, no member disclosure — but leaks receiver group names, and the hint isn't a valid send target.D — Opt-in live expansion (recommended). Default = A (never expand; safe floor). A receiver-admin toggle ("let peers see who's in receiver groups when discovering recipients") enables B where a real group→members source exists; otherwise free-text, optionally with C's hint. Live only; ∩ residents ∩
CanReceive; one audit event per call (already present).7. Recommendation — D, defaulting to A
Records the decision: discovery of group members is off by default, enabled per-instance by the receiver admin, live and source-gated, never materialized, never via
Manager.Users. Authorization is unchanged. Send-to-group fan-out (the Nextcloud model) is noted as a separate, larger future direction.Deliverable / next step
If D is ratified I'll cut the implementation issue: (i)
Registry.ReceiverGroups(peer)+ aGroupMembers(group)resolver interface implemented for keycloak-profile (group-members API) and LDAP, nil elsewhere; (ii) apeeringExpandGroupsreceiver setting (default false); (iii)ListRecipientsunions explicit users with live-expanded members ∩ residents ∩CanReceivewhen enabled; (iv) per-mode docs + the §4.3 threat-model note. Or say which of A / B / C you'd rather record instead.Refs: #125, #123, #96, #226; Nextcloud Federated Cloud Sharing (group-as-address model).
Decision recorded — Option D (ratified).
Group-receiver discovery is off by default, enabled per-instance by the receiver admin, resolved live and source-gated (real group→members query — Keycloak group-members API / LDAP; never
Manager.Users), never materialized, always ∩ residents ∩CanReceive. openbao / app-owned have no source and degrade to free-text. Authorization behaviour is unchanged. Send-to-group fan-out (the Nextcloud group-as-address model) is noted as a separate, larger future direction, not part of this.Implementation tracked in #243. Closing this deep-dive.