stash
This commit is contained in:
parent
c345372382
commit
395a9b156a
9 changed files with 1434 additions and 0 deletions
233
docs/design-in-progress/groups.md
Normal file
233
docs/design-in-progress/groups.md
Normal 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).
|
||||
Loading…
Add table
Add a link
Reference in a new issue