This commit is contained in:
j3d1 2026-08-20 05:04:30 +02:00
parent c345372382
commit 395a9b156a
9 changed files with 1434 additions and 0 deletions

View file

@ -0,0 +1,233 @@
# Groups (Design in Progress)
Status: not implemented. This document collects the problem, goals, and open design questions for
adding groups to Toolshed. Nothing here is settled; it's a starting point for discussion.
## What a group is
A group exists to model shared ownership of items, the way a club, workshop, or company owns
equipment collectively rather than any one person owning it. A group has members, and all members
have equal privileges to edit the items the group owns; there's no owner-vs-member distinction
within a group, membership itself is the privilege.
This is a sharper (and more consequential) definition than "a label you can share things with":
it means a group needs to be able to *own* things, not just receive shared access to them the way
a friend does. That has implications worked out below.
## Problem
The only relationship Toolshed currently models between users is pairwise friendship (see
[federation.md](../federation.md)), and every item has exactly one owning user. That's enough for
"I trust this one specific person" and "I personally own this thing," but it breaks down for
anything collectively owned:
- A shared workshop, tool library, or team can't own equipment as a unit. Today it has to belong
to one specific person's account, which is a poor fit and doesn't reflect who actually has a say
over it.
- There's no way for several equally-privileged people to edit the same item; edit rights today
are entirely tied to the single `owner` field.
- Adding or removing a member of an informal group currently means renegotiating friendships and
re-sharing individually; there's no shared object whose membership can just be edited once.
## Goals
- Let a set of users collectively own items, with every member holding equal edit rights over
those items.
- Let membership be managed in one place instead of via N pairwise arrangements.
- A request from a client to any backend may not depend on any other backend being online at the
same time — not to construct the request, and not to verify it. Concretely: the receiving server
must be able to verify the request using only the request itself plus keys it has already cached
from prior trust (friend-accept), with the group's own authoritative backend and the requesting
member's home server both unneeded and unreachable-safe at verification time; and the client must
be able to send the request using only what it already has cached, with the group's authoritative
backend unneeded and unreachable-safe at send time too. This is the same property plain user
requests already have (see federation.md's Cryptography section); group requests must not regress
it on either side.
- Fit into the existing handle system (see federation.md's "Unique Handles" section): a group
should be nameable and referenceable using the same handle shape a user is.
- Stay optional and additive. Pairwise friendship and single-user ownership should keep working
exactly as they do now for people who never touch groups.
## Non-goals (for now)
- Group governance beyond flat, equal membership (voting, roles, hierarchies). Equal privileges
for all members is the whole model for now; anything more layers on top later if needed.
- Fine-grained per-item permissions within a group (e.g. "this member can edit but not delete").
Membership is the only privilege level.
## Open design questions
### Does a group need its own keypair?
No. A group's day-to-day existence is a membership roster maintained by whichever backend is
authoritative for the group's handle (the same "authoritative backend" idea a user's domain already
implies, see federation.md's Servers subsection). A request made "as the group" is an ordinary
request, signed with an actual member's own personal private key, together with a claim of which
group it's acting on behalf of (`acting_as`), signed as part of the same payload as the rest of the
request.
A receiving server must be able to authenticate such a request using only what arrives in the
request plus keys it already holds; no other server, including the group's own authoritative
backend, needs to be reachable at verification time. This is accomplished with **membership
certificates**: on request, a group's authoritative backend issues a current member a small
signed statement of the shape "handle X, public key P, is a member of group #G, valid from T1 until
T2," signed with the group's own private key. Issuance is on-demand (the member asks, rather than
the backend pushing renewals on a schedule), but it is its own separate action, decoupled from
sending any particular group request: a member fetches and refreshes this certificate from the
group's backend whenever they happen to be online, caches it locally, and later attaches whichever
certificate they currently hold to a request they make "as the group." Sending that request never
itself triggers a live fetch from the group's backend — if the cached certificate has expired and
the group's backend isn't reachable right then, the request simply can't be sent as the group yet;
the client doesn't fall back to contacting the group's backend synchronously to get one.
A receiving server checks two signatures, using keys it already has cached, with no outgoing call
to anyone: the member's signature over the request, using the public key embedded in the
certificate itself, and the certificate's own signature, using the group's public key, learned and
cached exactly the way any friend's key is, at the point the group was friended. If both check out
and the certificate hasn't expired, the request is authorized.
The certificate is what lets a receiving server trust a specific member's public key at all, for
members it has never individually friended: that trust is vouched for by the group's already-cached
key, rather than requiring a separate key-exchange with every member of every group a user happens
to be friends with. Equal privileges for all members falls out of this directly, since any member's
own key plus a valid certificate is sufficient proof.
Removing a member takes effect once their existing certificate expires, not the moment the
backend's roster is edited; certificate lifetime is the parameter that governs how quickly a
removal actually takes hold (see "How long should a membership certificate be valid for?" below,
and Security below).
### Are group handles different from user handles, or is a group just a special kind of user?
The handle should look almost exactly like a user handle, just prefixed with `#`: `#groupname@domain`
instead of `groupname@domain`. It's resolved the same way and referenceable in the same places (e.g.
as an item's owner, or as a friend-list entry), so the reuse of the existing federation model is
unaffected. The prefix exists only to keep group and user handles from occupying the same namespace on
a domain: without it, "is `groupname@domain` a user or a group" would depend on which one happened to
register the name first, and the two could never be told apart just by looking at the handle. With the
prefix, a domain can have both a `climbing@domain` user and a `#climbing@domain` group with no
collision and no ambiguity about which is which, and any code path that resolves a handle can dispatch
on the actor kind (user vs. group) from the handle's own shape, before it even needs to ask a server.
But underneath, a group isn't really "a special kind of user," it's backed by a fundamentally
different kind of identity: a membership list instead of a single keypair (see above). A user
handle answers "is this request authentically from this one identity," a group handle answers "is
this request from someone currently entitled to act for this collective identity." Those are
different questions even though the answer to both ends up being "yes, forward the request." Groups
and users are probably best thought of as two kinds of actor that share a handle format and most of
the surrounding plumbing (ownership, friendship, availability policy), rather than one being a
special case of the other.
### Should a group be able to grant read access to non-members ("group friends")?
Yes, this should reuse the same mechanism a user's own sharing already uses. Since a group is an
actor with a handle, it can have its own friends the same way a user does, and a group's owned
items can go through the same availability-policy check (private vs. visible to the group's
friends) that an individual's items already do. Nothing new needs to be invented here, it's the
existing friendship and availability-policy machinery applied to a second kind of actor.
### Should groups be able to befriend other groups?
Yes, for the same reason: if a group is an actor with a handle and a friends list, there's no
reason the other side of that friendship has to be a user specifically. Two clubs befriending each
other so each can see the other's shared equipment is the same mechanism as two users befriending
each other, just with both sides being groups instead of one or zero.
### Should anyone be able to share directly with a specific group, instead of with "my friends" generally?
This is the one piece that isn't just reuse of what exists today. Right now, an item's
availability policy is all-or-nothing across *all* of the owner's friends, there's no way to share
with a subset of friends, individually or as a named group. Letting an item be shared with one
specific group (a user's own item, shared with a club they belong to, say) instead of with every
friend equally would be a genuine generalization of the current sharing model, not something that
falls out of adding groups as an actor.
Worth noting: this capability would be just as useful for individual users wanting to share with a
subset of their friends, without a group being involved at all. It might make more sense to design
"share with a specific target (user, group, or named subset)" as its own piece of work, rather than
building it as a groups-only feature.
### How long should a membership certificate be valid for?
This is the tuning knob the certificate design introduces, and it's a real trade-off rather than a
detail to defer. A short validity window (say, hours) keeps the staleness window after a removal
small, but means a member who's offline for longer than that can't act as the group at all until
they reconnect and refresh. A long window (days or weeks) is more forgiving of intermittent
connectivity but leaves a removed member's old certificate usable for longer. Whatever default is
picked, a member should be able to fetch a fresh certificate well before the old one expires while
still online, so the common case isn't "offline for exactly the wrong amount of time."
## Interaction with availability policy
Items currently have an availability policy (private / share / lend / rent / sell) that's a
property of the item, not a list of who it applies to, "friends" is implicit and applies equally to
all of them. A group-owned item works the same way, just with the group's own friends as the
implicit audience instead of an individual's. Targeted sharing (the question above) would extend
this, not replace it.
## Known gaps in the design
None of these block the design, but they're real gaps that need an explicit answer before
implementation.
### Ambiguity
- **What exactly is signed.** The `acting_as` claim and the certificate's identifying fields must be
signed as part of the same payload the member's key signs, not as free-standing, unsigned data
alongside it. If they aren't inside the signed bytes, `acting_as` can be swapped after signing,
turning a personal request into a group one or vice versa, or one group's request into another's.
- **Who can change membership, and how.** Equal privileges to edit items doesn't by itself say
whether that equality extends to *requesting or renewing certificates for others, or editing the
authoritative backend's issuance list itself*. Flat and unilateral (any member can add or remove
any member) is the simplest reading of "no owner-vs-member distinction," but it's a materially
different trust model from "equal edit rights over items" and deserves its own explicit decision.
### Security
- **Confused deputy on `acting_as`.** Trust in an `acting_as` claim reduces to trust in the group's
authoritative backend's issuance decisions: a malicious or compromised backend can sign a
certificate for a handle that was never really a member, and every receiving server that trusts
the group's key will accept it. The backend's issuance discipline, and the security of its own
private key, is a single point of failure for the group as a whole.
- **Membership staleness window.** There is a window after a member is removed during which their
existing certificate keeps working: exactly the certificate's remaining validity period. This is a
strictly worse revocation story than individual friendship, where trust is keyed to a public key
learned once with no expiry, but the window is a bounded, chosen parameter (see "How long should a
membership certificate be valid for?" above) rather than open-ended.
- **Blast radius of a single compromised member key.** Because membership is flat and unilaterally
editable by any member, a compromised personal key doesn't just expose that person's own items,
as with an ordinary account compromise, it exposes edit rights over everything the group owns for
as long as that member's certificate remains valid, and can be used to obtain a certificate for an
attacker-controlled handle as a permanent member before anyone notices. This risk is inherent to
"equal privileges, no roles" as a model, worth flagging even though richer governance is a
non-goal for now.
- **Self-lockout / orphaning.** Nothing in the design stops a group's last member from leaving (or
removing everyone else) from the backend's issuance list, which would leave group-owned items with
no one able to obtain a valid certificate for that owner at all once existing certificates expire.
The backend should guard against removing the last member, but that guard doesn't address a
member unilaterally removing every *other* member, which the flat model otherwise permits.
- **Audit trail depends on discipline.** Since any member's signature plus a valid certificate
satisfies authorization, "the group edited this item" is never sufficient for an audit trail; the
actual signer's handle (from the certificate's embedded public key) must always be logged
alongside the group claim, or member-level accountability is lost entirely.
### Maintainability
- **Two actor kinds sharing one code path.** `Group` should have the same shape as `User` for the
things that matter (a `.friends` set, a `.handle`), so ownership/friending/availability-policy
code can stay actor-agnostic. That reuse only holds if future code is disciplined about not
special-casing `User` in ways that assume a single, non-expiring keypair (e.g. "cache the owner's
public key forever, no expiry check needed") — a shortcut that would silently break the moment
the owner turns out to be a group, where the *acting member's* key is only good until its
certificate expires.
- **Certificate issuance and refresh is a client responsibility.** A member's client needs to
refresh its certificate before it expires, handle in-flight group actions failing closed if it
didn't (the same as any expired-credential error), and surface refresh failures to the user
rather than as a confusing rejected request.
- **Expanded federation test surface.** Every existing federation test implicitly assumes the
request's signer and its authorized actor are the same handle. `acting_as` plus an embedded
membership certificate means the whole request-verification path needs testing for the
signer-vs-actor split and the certificate's own signature and expiry checks, including
cross-domain cases (group hosted on one domain, member's key registered on another, item owned by
the group sitting on a third) and expiry-boundary cases (certificate expires mid-flight, is
refreshed concurrently with a request, etc).