toolshed/docs/design-in-progress/groups.md
2026-08-20 05:19:53 +02:00

16 KiB

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), 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).