Public Access
Merge branch 'main' into claude/angry-cray-f3d496
# Conflicts: # README.md
This commit is contained in:
@@ -226,8 +226,8 @@ bastion — the last needs jump hosts the connection layer has not got. All thre
|
||||
|
||||
### Working as a team
|
||||
|
||||
**TEAMS** in the nav rail creates a team, adds members and shares vaults. One distinction runs through the
|
||||
whole screen and is worth having before you use it.
|
||||
**TEAMS** in the nav rail creates a team, adds and invites members, shares vaults, hands a team over and
|
||||
archives one. One distinction runs through the whole screen and is worth having before you use it.
|
||||
|
||||
**Adding somebody to a team and giving them a key are two different acts, and only the first is something
|
||||
the server can do.** Adding a member changes what the server will *serve* them: the team's vaults appear in
|
||||
@@ -244,7 +244,58 @@ published in a log every other client also reads. **It does not prove the key is
|
||||
Compare the fingerprint with them over something this server does not carry; that is the only step that
|
||||
closes it, and the success message says so every time.
|
||||
|
||||
Three limits, stated rather than discovered:
|
||||
**Somebody with no account here yet can be invited, and nothing is sent.** There is one button — **ADD
|
||||
MEMBER** — and it does whichever of the two applies, because which one applies is a fact about the server's
|
||||
account table rather than about what you are trying to do. If the directory knows the address, that account
|
||||
is added straight away. If it does not, the address is invited instead, and the status line says which
|
||||
happened, because the difference decides what you do next.
|
||||
|
||||
An invitation is a standing instruction rather than a message: the next account that signs in with that
|
||||
address joins this team, at the role you chose. There is no link and no token, because this server has no
|
||||
outbound mail path and does not pretend otherwise — telling them to go and sign in is your job, over a
|
||||
channel this server does not carry, and a link nobody can deliver would be worse than no link. An
|
||||
invitation lasts fourteen days, so an address handed on to whoever takes the job next does not carry a
|
||||
standing offer for ever; it can be withdrawn until it is taken up; and like adding a member it grants
|
||||
nothing readable, so somebody still has to press SHARE KEY afterwards.
|
||||
|
||||
The one thing the merged button costs is worth knowing. Adding an account the directory knows also hands
|
||||
you the public key you are about to verify and wrap a vault to, and an invitation cannot do that because
|
||||
there may be no key yet. So when you are adding somebody *in order to* share a vault with them, the useful
|
||||
sequence is still the same one: add them, see them appear in the members list, then share.
|
||||
|
||||
Inviting an address that already belongs to a member of the team is refused and says so. Inviting one that
|
||||
merely *has* an account here is not — that would make this a way of asking the server which addresses have
|
||||
accounts, which is not a question anybody willing to create a team first should be able to put to it. Such
|
||||
an invitation simply gets claimed sooner: within the hour, on the same sweep that records they were here,
|
||||
rather than waiting for a first sign-in that has already happened.
|
||||
|
||||
**An invitation is only claimed if your identity provider says the address is verified, and there is no way
|
||||
to relax that.** The access token has to carry `email_verified` as true. Anything else — false, missing, or
|
||||
sent under another name — claims nothing at all, and no setting turns that off: an invitation decides what
|
||||
the server will serve, and one that could be taken by anybody able to obtain a token asserting somebody
|
||||
else's address is a way into a team. **If your invitations never activate, this is the first thing to
|
||||
check.** They sit at *pending* rather than failing, the server logs a warning each time it declines to
|
||||
claim one, and the two fixes are on your side: set `Oidc:EmailVerifiedClaim` to whatever your provider
|
||||
calls the claim if it is not `email_verified`, and make sure the provider puts it in the **access** token
|
||||
rather than only in the ID token or the userinfo response.
|
||||
|
||||
**Ownership is sole, and handing it over is one act.** Transferring names an existing active member: they
|
||||
become owner and you become an admin, in a single transaction. Not two role changes — promoting first
|
||||
leaves the team owned twice, demoting first leaves it owned by nobody, and there is nobody with the
|
||||
authority to finish a transfer that stopped in the middle. You are demoted rather than removed, so you keep
|
||||
your vault key grants; removing you would revoke them and flag every team vault for rekey, and somebody
|
||||
handing over a team is usually staying in it.
|
||||
|
||||
**Archiving a team is refused while it owns a vault, and that is a limit rather than a rough edge.** A team
|
||||
vault is readable *because* of membership, so archiving a team that still owned vaults would take them away
|
||||
from everybody holding a key — including you — quietly and all at once. Nothing in this product deletes a
|
||||
vault, so there is no order of operations that gets past the refusal today, and it says so with a count of
|
||||
what is in the way rather than failing vaguely. Archiving an empty team takes its memberships and its
|
||||
outstanding invitations with it, in one transaction. Its name can be changed whenever you like; its slug
|
||||
cannot, because a slug is unique only among live teams and a rename could take one an archived team is
|
||||
still holding.
|
||||
|
||||
Four limits, stated rather than discovered:
|
||||
|
||||
- **Removing a member is not retroactive.** It revokes their grants and flags the team's vaults for rekey,
|
||||
and blocks future reads. Everything they already pulled is on their machine. Rotate the SSH credentials
|
||||
@@ -255,6 +306,12 @@ Three limits, stated rather than discovered:
|
||||
then trust silently for a host you defined. The cost is that each member approves a team host's key once
|
||||
on each of their machines. Team vaults' pins are still *listed* on the Vault screen, so you can see what
|
||||
has been trusted.
|
||||
- **LAST ACTIVE is coarse on purpose.** The server records it at most once per account per hour, so a value
|
||||
an hour old means "recently" and not "at that moment". That is the granularity the question is really
|
||||
asked at — whether somebody is still using this deployment — and writing it on every request would put an
|
||||
UPDATE on the hot path of every authenticated call for a number nobody reads that closely. It is shown
|
||||
as roughly-when rather than to the minute, because showing it to the minute would be reading a precision
|
||||
into it that is not there.
|
||||
|
||||
Items are filed into one vault at a time. When more than one vault is writable, the host and vault editors
|
||||
show a picker; it defaults to your personal vault and never moves on its own, because an item put in a team
|
||||
@@ -274,6 +331,12 @@ that design now; the desktop's own v2 is a 190-pixel labelled sidebar in place o
|
||||
search box in the titlebar, and session tabs as pills. Its light theme is not built — see
|
||||
[`docs/design-import-gaps.md`](docs/design-import-gaps.md) — so the application is dark on both.
|
||||
|
||||
**A connected phone shows one bar and then the terminal.** The header, the session strip and the bottom
|
||||
bar are collapsed while a shell is up, and a single 35-pixel row replaces them: back on the left, the
|
||||
sessions as pills, and a `+` on the right offering the three connections this application can make — a
|
||||
shell, a host's files over SFTP, or a bucket. The system back gesture does what the arrow does, and lowers
|
||||
that menu first if it is open.
|
||||
|
||||
Widening the sidebar moved the desktop window's minimum from `880x560` to `1016x574`, which leaves every
|
||||
screen exactly the width it was designed against.
|
||||
|
||||
@@ -282,11 +345,16 @@ either an SFTP host or a bucket. There is no local pane, because there is no bro
|
||||
put in one — moving a file *in* from the phone needs the system document picker and is the next piece of
|
||||
work rather than a thing the screen pretends to do. Hosts and groups are made and corrected here now, from
|
||||
a floating + on the Hosts screen, and both editors are cards in the list's own row rather than dialogs, so
|
||||
the form never covers the thing it is about. The keychain has no editor of its own: SSH keys, passwords and
|
||||
buckets are created on the desktop and sync down, and the phone will delete one — behind the same counted
|
||||
confirmation — without offering to change it. The one item this head makes is a tag, from inside a host's
|
||||
editor where tagging is what you were doing anyway; renaming one is still a desktop job. Pins, teams and
|
||||
import have no phone screen either. Importing an `~/.ssh/config` has no meaning on a phone at all.
|
||||
the form never covers the thing it is about. The keychain has no editor of its own: SSH keys and buckets are
|
||||
created on the desktop and sync down, and the phone will delete an item — behind the same counted
|
||||
confirmation — without offering to change it. What this head does make, it makes where the need arises
|
||||
rather than in an editor: a tag from inside a host's editor, and a credential from the connect bar's
|
||||
remember tick, which stores the password just typed and moves the host onto it. Renaming either is still a
|
||||
desktop job. Pins and import have no phone screen either, and importing an `~/.ssh/config` has no meaning
|
||||
on a phone at all. **TEAMS does have one**, behind MORE, and it is there for a reason the design could not
|
||||
have anticipated: an invitation is claimed by signing in, so somebody being told they have been put in a
|
||||
team is at least as likely to be holding a phone as sitting at a desktop, and a membership visible only on
|
||||
a head they have not installed is a membership they cannot see.
|
||||
|
||||
**Port forwarding is not built anywhere**, and the phone's More screen says so in a paragraph rather than
|
||||
leaving a gap. The v2 design draws a whole screen for it; nothing in the SSH layer forwards anything, so
|
||||
@@ -488,7 +556,10 @@ keychain plus a terminal — and the spike that gates all of it.
|
||||
append-only key log served for clients to verify against, team-owned vaults, and vault key grants
|
||||
wrapped by a client and stored opaquely by the server. `VaultAccessService` now resolves team
|
||||
membership to permissions, so a viewer may pull and may not push; the desktop client reads and syncs
|
||||
every vault it holds a key for, and a real TEAMS screen replaces the placeholder. See
|
||||
every vault it holds a key for, and a real TEAMS screen replaces the placeholder. A team can be renamed,
|
||||
handed to another member, and archived once it owns no vaults; a member row carries when that account was
|
||||
last here; and an address with no account on this deployment can be invited, joining the moment somebody
|
||||
signs in with it. See
|
||||
[Working as a team](#working-as-a-team) for the one distinction the whole design rests on, and the limits worth
|
||||
knowing before you rely on it; the reasoning is in
|
||||
[ADR 0009](docs/adr/0009-team-access-model.md).
|
||||
@@ -497,8 +568,14 @@ keychain plus a terminal — and the spike that gates all of it.
|
||||
every team vault `RekeyRequired`, and nothing acts on that flag. A rekey re-wraps every item's data key
|
||||
under a fresh vault key and can only be performed by a client that holds the current one; that is M5's
|
||||
key rotation. Until it lands the flag is what the interface reads to say a rotation is owed, which is
|
||||
more honest than a button that only appears to do it. Ownership transfer is absent for the same kind of
|
||||
reason — the owner cannot be removed or demoted, because nothing can appoint a replacement.
|
||||
more honest than a button that only appears to do it.
|
||||
|
||||
**Ownership transfer is here, and it is one write rather than two.** The member you name becomes owner
|
||||
and you become an admin, in a single transaction — because ownership is sole, so promoting first leaves
|
||||
the team owned twice and demoting first leaves it owned by nobody, and there is nobody left with the
|
||||
authority to finish a transfer that stopped in the middle. Nothing else is touched: you keep your vault
|
||||
key grants, because removing the outgoing owner would revoke them and flag every team vault for rekey,
|
||||
which is a much larger act than the one being asked for.
|
||||
- **M4 — hardening and ops**, packaging, self-hosting guide.
|
||||
- **M5 — multi-provider OIDC**, key rotation, per-item content keys.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user