Public Access
Make the vault the thing you share, and ask a host which one it lives in
The teams screen listed teams that owned vaults, so sharing four servers with two
colleagues meant creating a team, then a vault inside it, then wrapping a key.
Two of those three steps are about a concept nobody arrives wanting. The screen
now lists vaults: naming one creates the membership list that carries it, named
after the vault and owned by you, and members, invitations, roles, hand-over and
key holders all hang off the vault they apply to.
Nothing on the server moved. VaultAccessService still resolves a shared vault
through team_membership and every membership call still names a team id — what
went is the requirement that anybody make one. The split the whole design rests
on is untouched and is still what the screen is built around: adding somebody
authorises the server to serve them, and only a machine holding the key can make
the vault readable. ADR 0009 keeps its decision and gains an addendum recording
which half of it a person is now asked about.
The one place the team resurfaces is a membership list carrying several vaults,
which this screen cannot produce and does not hide: the members section says so,
because "adding somebody here adds them there" is precisely the fact a
vault-shaped screen is in a position to conceal.
Two things left the interface and one arrived. Creating a team is gone, and so is
archiving one — it was only ever possible for a team owning no vaults, and a
screen whose rows are vaults has no row for one, so the button would have been
unreachable or always refused. The endpoint is unchanged and the screen states
the limit instead, since a vault cannot be deleted at all. The exception is a
create whose second call failed: cancelling that form archives the membership
list it left behind, which is a deliberate departure from this client's rule
against tidying up on the user's behalf, made because nothing else can reach it.
What arrived is PUT /api/v1/vaults/{id}. Without it the screen loses its only
editing action, since renaming the team behind a vault is invisible to everybody
who was never shown the team. It is gated on PermissionFlags.Admin — the line
UpdateTeamEndpoint already draws, because a name is what everybody in the vault
sees it called rather than part of its contents — and it renames the owning team
with it when that team carries nothing else, so the row an operator reads and the
name a user says cannot drift apart. The slug never moves, for the reason it does
not move on a team rename. The session edits its cached vault row rather than
replacing it with the response, which deliberately carries no wrapped key.
The host editor now asks which vault a host goes into, beside the name, while
adding and only where there is more than one vault to write to. It is a second
picker rather than the keychain screen's reused, and the two selections are
separate on purpose: that one is a standing preference about where new items go,
this is a field of the host in front of you, and binding both to one selection
would mean a click on the other screen could move a half-typed host. An existing
host is not offered it at all rather than offered it disabled — the two vaults
are encrypted under different keys, so moving an item is a delete and a retype.
That forced a fix worth naming. The group picker was built from the active
vault's groups whatever vault the host was being filed into, so a host put in a
shared vault could be filed under a group only its author can resolve — a
colleague would see it filed under nothing, which is the quietest kind of wrong.
Groups are now kept per vault and the picker follows the vault choice.
Two renames, because the pair they would otherwise have made is a bug farm:
ShellScreen.Vault became Keychain and VaultScreen became KeychainScreen, which is
what the rail has always labelled that screen, leaving Vault for one vault's
contents and Vaults for the vaults themselves. The enum values are unchanged;
NavRail.axaml writes them as x:Static literals.
1536 tests pass, seven more than before. Five are new on the server — the rename
endpoint's success, the team it does and does not take with it, the two refusals
and the empty name — and the client suite gains six and folds four together,
having lost the two about archiving a team.
This commit is contained in:
@@ -55,7 +55,7 @@ Three of those fields do not exist at any layer, and one of them is refused on t
|
||||
| Asked for | What exists today |
|
||||
| --- | --- |
|
||||
| The `+` and the editors behind it | Nothing. `Theme/Phone.axaml` has no `.fab` class, and its comment says an unused style would be "a claim that the control exists somewhere". `HostsScreen.axaml`'s own v2 note says hosts are created on the desktop and sync down. *(This row named a `ConnectionsScreen.axaml` as a second site. No such file exists or ever has; and the `HostsScreen` statement is an XAML comment, so no phone screen ever rendered that sentence to a user.)* |
|
||||
| Vault picker | **Built.** `VaultViewModel.TargetVaults` / `SelectedTargetVault` / `HasVaultChoice`, hidden at one vault. The desktop's `VaultScreen.axaml` already draws it. |
|
||||
| Vault picker | **Built.** `VaultViewModel.TargetVaults` / `SelectedTargetVault` / `HasVaultChoice`, hidden at one vault. The desktop's keychain screen already draws it. |
|
||||
| Alias, hostname, port, username, key-or-password, group | **Built**, in the shared `VaultViewModel` host editor — `EditorLabel`, `EditorHostname`, `EditorPort`, `EditorUsername`, `EditorAuthenticationChoices`, `EditorGroupChoices`, `SaveHostCommand`. The phone has never bound any of it. |
|
||||
| Tags | **Nothing.** `SyncEntityType.Tag = 5` and `HostTag = 6` are reserved slots with nothing behind them. `HostSecret` has no tag field. |
|
||||
| A group's parent | **Refused on the record.** `HostGroupSecret`'s own remark says groups are flat because two clients can each re-parent A under B and B under A offline, a scalar merge accepts both, and the result is a cycle no reader can draw and the server cannot see, because it is inside the payload. |
|
||||
|
||||
@@ -115,6 +115,36 @@ Three decisions inside it belong here, because each had a more convenient altern
|
||||
an address already belonging to a member of *this* team is refused, and that is a fact the caller can
|
||||
already read off the members table, so naming it leaks nothing.
|
||||
|
||||
### Addendum: the vault is what the product shows, and the team is behind it
|
||||
|
||||
The model above is unchanged. What changed afterwards is which half of it a person is asked about.
|
||||
|
||||
The first interface built on this ADR made the team the subject: you created a team, then a vault in it,
|
||||
then wrapped a key. Two of those three steps are about a concept nobody arrives wanting. So the screen now
|
||||
lists **vaults**, and naming one creates the membership list that carries it — named after the vault,
|
||||
owned by the creator, one per vault. Nothing on the server moved: `VaultAccessService` still resolves a
|
||||
shared vault through `team_membership`, every membership call still names a team id, and the split this
|
||||
ADR is about — membership authorises, a grant unlocks — is still what the screen is built around, now
|
||||
stated per vault rather than per team.
|
||||
|
||||
Three consequences of the change belong here:
|
||||
|
||||
- **A team owning several vaults is still legal and is no longer produced.** The client cannot make one;
|
||||
an operator or a pre-existing deployment can. The screen refuses to hide it: a vault whose membership
|
||||
list carries others says so, because on a vault-shaped screen "adding somebody here adds them there" is
|
||||
precisely the fact that would otherwise be invisible.
|
||||
- **Archiving left the interface.** It was only ever possible for a team owning no vaults, and a screen
|
||||
whose rows are vaults has no row for one — so the button would have been unreachable or always refused.
|
||||
The endpoint is unchanged and the screen states the limit instead. The one place a vault-less team can
|
||||
still appear is a create whose second call failed; cancelling that form archives it, which is a
|
||||
deliberate exception to this client's rule against tidying up on the user's behalf, made because nothing
|
||||
else can reach it.
|
||||
- **A vault can be renamed**, which it could not before: `PUT /api/v1/vaults/{id}` requires
|
||||
`PermissionFlags.Admin` — the line `UpdateTeamEndpoint` already draws, because a name is what everybody
|
||||
in the vault sees it called rather than part of its contents. It renames the owning team with it when
|
||||
that team carries nothing else, so the row an operator reads and the name a user says do not drift
|
||||
apart. The slug never moves, for the reason it never moves on a team rename.
|
||||
|
||||
## Consequences
|
||||
|
||||
The sharing graph is visible to the operator: who is in which team, which vaults exist, and who holds
|
||||
|
||||
+29
-17
@@ -29,16 +29,16 @@ the chrome, hosts and terminals, file transfer, the vault, teams, and preference
|
||||
> over a view model that already existed, plus preferences. `ShellScreen` gained `More` and `Buckets`;
|
||||
> SFTP and S3 are one screen over one `TransfersViewModel`, differing only in which picker they offer.
|
||||
>
|
||||
> **A sixth is behind MORE that v2 never drew: TEAMS.** It is the reverse case — a shipped screen the
|
||||
> **A sixth is behind MORE that v2 never drew: VAULTS.** It is the reverse case — a shipped screen the
|
||||
> design had no slot for — and it is on the phone for a reason the design could not have anticipated,
|
||||
> because invitations did not exist when it was drawn. An invitation is claimed by *signing in*, and the
|
||||
> person being invited is at least as likely to be holding a phone as sitting at a desktop; a team the
|
||||
> person being invited is at least as likely to be holding a phone as sitting at a desktop; a vault the
|
||||
> server has just put somebody in, visible only on a head they may not have installed, is a membership
|
||||
> they cannot see. It runs over the same view model the desktop screen drives, like the other four.
|
||||
>
|
||||
> | v2 element | What ships instead |
|
||||
> | --- | --- |
|
||||
> | The **FORWARDING** screen: local/remote/dynamic rules, toggles, bytes transferred | **Nothing, said out loud.** `ISshConnection` offers `OpenShellAsync` and nothing else, so there is no tunnel for a rule to run through; `SyncEntityType.PortForward = 9` is still reserved and still unused. The MORE screen carries a paragraph naming the absence, for the reason the desktop keeps TEAMS in its rail. |
|
||||
> | The **FORWARDING** screen: local/remote/dynamic rules, toggles, bytes transferred | **Nothing, said out loud.** `ISshConnection` offers `OpenShellAsync` and nothing else, so there is no tunnel for a rule to run through; `SyncEntityType.PortForward = 9` is still reserved and still unused. The MORE screen carries a paragraph naming the absence, for the reason the desktop keeps VAULTS in its rail. |
|
||||
> | `23 ms · fwd 5432` on the terminal's connection line | ◆ **The line is gone, and what was real on it moved.** There was never an RTT to draw — SSH.NET measures none — and nothing forwards anything, so what shipped was the account and endpoint actually dialled. In v3 a connected phone draws one 35-pixel bar and then the terminal, so a second 36-pixel row naming the machine is exactly the chrome that surface exists to give back: the address is on the connecting card, where it is read before anything has answered, and the shell's own prompt says it afterwards. The two text-size buttons that shared the line are pinned at the end of the accessory row, outside its scroller, which is what the line was protecting them from. |
|
||||
> | `ED25519` badge and `SHA256:kQ9f…Zw2M` on every keychain card | `Detail`, which is what is genuinely known *about* an item. Unchanged from the first import: no algorithm field, no fingerprint, and computing either means parsing armour the type stores verbatim. |
|
||||
> | An `agent` chip on a key | Omitted. There is no agent of any kind — see the first import's Vault section. |
|
||||
@@ -84,7 +84,7 @@ the chrome, hosts and terminals, file transfer, the vault, teams, and preference
|
||||
> | **Split ⌘D** | Still omitted — the renderer stacks panes and shows one; tiling needs a pane geometry it has not got. |
|
||||
> | macOS traffic lights, and `⌘K` | The window's own minimise/maximise/close, and `CTRL K`. Development is Windows-first and the chrome is `BorderOnly` for a documented reason. |
|
||||
> | No status bar | Kept, and cut down to the one thing the titlebar does not now carry: `Vault.Status`, which is the only channel this application has for saying a save failed or a merge picked a winner. The design is a mock-up of a working afternoon and has nowhere to put a sentence like that. |
|
||||
> | The sidebar's five destinations, and a **Team vault** card at its foot | Seven destinations, because Pins, Teams and Preferences are built screens and dropping their entry would strand them — and two fewer than v2 shipped with, because SFTP and S3 became tabs; see v3 below. The card is not drawn: it is a second route to a screen already in the list, carrying a seat count nothing here produces. |
|
||||
> | The sidebar's five destinations, and a **Team vault** card at its foot | Seven destinations, because Pins, Vaults and Preferences are built screens and dropping their entry would strand them — and two fewer than v2 shipped with, because SFTP and S3 became tabs; see v3 below. The card is not drawn: it is a second route to a screen already in the list, carrying a seat count nothing here produces. |
|
||||
>
|
||||
> ## The desktop's v3
|
||||
>
|
||||
@@ -157,7 +157,7 @@ the chrome, hosts and terminals, file transfer, the vault, teams, and preference
|
||||
> | **Add Telnet**, and **Serial** in the toolbar | Omitted. `ISshConnection` is the only transport there is. This is also why the card subtitle's `ssh` is a constant today rather than a reading — it is stated in `HostRowViewModel.Summary`, which is the one place in this interface where a constant is printed on purpose. |
|
||||
> | **+ SSH ID, Certificate, FIDO2** | Omitted. `IDENTITIES` and `CERTIFICATES` have been on this document's list since the first import — neither is even a reserved `SyncEntityType` — and there is no security-key path anywhere in the SSH layer. One control offering three item types that do not exist. |
|
||||
> | The **Backspace / Default** row | Omitted. It is a terminal setting, and the client has no preferences store and no frame to carry one to the renderer — see the Preferences section. It would be a control whose value could not survive the window closing. |
|
||||
> | The **chevron beside the vault name** | The name alone. An item cannot be moved between vaults: the two are encrypted under different keys, so moving one is a delete and a retype. Where a *new* item is filed is chosen on the keychain screen, which is the only vault question with an answer. |
|
||||
> | The **chevron beside the vault name** | The name alone, on the pane about an existing host: an item cannot be moved between vaults, because the two are encrypted under different keys and moving one is a delete and a retype. The half of the question that *does* have an answer — where a new host goes — is asked in the host editor, as a picker beside the name; keys, passwords and buckets take theirs from the keychain screen's standing picker instead. |
|
||||
> | **Show more ⌄** | Not drawn as a disclosure. What it would hide — notes, the relay switch, forgetting the host key — is in the editor, one press away, and a second fold inside a pane that already scrolls is a second place for a field to be missing from. |
|
||||
> | **Port Forwarding** in the sidebar | Nothing, for the third time in this document. |
|
||||
> | The host grid's toolbar avatar, share and tag-filter controls | Omitted, as in v3 and for the same reasons. |
|
||||
@@ -188,7 +188,7 @@ this document where what shipped differs from what the row predicted.
|
||||
**~~Teams are schema and nothing else.~~ Built in M3.** The `team` and `team_membership` tables were there
|
||||
from the first migration with nothing reading them, and `VaultAccessService.ResolveAsync` denied every vault
|
||||
that was not the caller's own. Both changed in M3 and neither needed a migration, which is what carrying two
|
||||
unused tables bought. See [Teams](#teams). What has *not* changed is the split underneath: the server
|
||||
unused tables bought. See [Vaults](#vaults). What has *not* changed is the split underneath: the server
|
||||
decides what it will serve, and only a client can decide who can read it — so "shared with" is two facts on
|
||||
this screen, not one.
|
||||
|
||||
@@ -282,7 +282,7 @@ field cannot be removed and stays as a permanently refused member; `SyncEndpoint
|
||||
|
||||
| Design element | Layer | What it would take | What ships instead |
|
||||
| --- | --- | --- | --- |
|
||||
| Org chip `dodotech / platform` | contracts + server | An *organisation* above teams, which does not exist — `VaultSummary.TeamId` is no longer always null since M3, but a team is not an org and there is exactly one tenant per deployment. | The vault's own name, and the account this machine is enrolled as. Team names are on the TEAMS screen, where they are about something. |
|
||||
| Org chip `dodotech / platform` | contracts + server | An *organisation* above teams, which does not exist — `VaultSummary.TeamId` is no longer always null since M3, but a team is not an org and there is exactly one tenant per deployment. | The vault's own name, and the account this machine is enrolled as. Team names are on the VAULTS screen, where they are about something. |
|
||||
| `SYNCED` dot, always green | client-app | Nothing — the design's claim is simply unconditional. | Green **only** when a connection is held, the last sync pass actually reached the server, and the outbox is empty; otherwise `UNREACHABLE`, the count of changes still waiting, or `OFFLINE`. Holding an `IVaultServer` proves a sign-in once succeeded and nothing more, so a laptop whose lid has been shut all afternoon still has one — reachability comes from the outcome of the last pass. A permanently green light is the same as no light. |
|
||||
| `VAULT SYNCED 11:02` | client-session | `StoredSyncState.LastPulledAt`/`LastPushedAt` are persisted, but `VaultSession` exposes the store as `internal`. A property away. | Omitted. The one honest sync fact — the outbox depth — is in the titlebar and the status bar. |
|
||||
| `⌘K` command palette running commands | client-domain | A snippet or saved-command item type (`SyncEntityType.Snippet = 8` is reserved). | Ctrl+K opens a real host search that connects on Enter. The box says "search hosts", not "search hosts · run command". |
|
||||
@@ -358,15 +358,17 @@ transfer primitive. The queue does its own 64 KiB copy loop and shares nothing w
|
||||
|
||||
---
|
||||
|
||||
## Vault
|
||||
## Keychain — the design's Vault screen
|
||||
|
||||
The screen ships and is real: four categories over the vault's four item types, one table, a detail pane,
|
||||
and both editors. What follows is what the design drew around them.
|
||||
and both editors. It is called the Keychain in both heads, which is what the rail has always labelled it;
|
||||
the section below keeps the design's word only where it quotes the design. What follows is what the design
|
||||
drew around them.
|
||||
|
||||
| Design element | Layer | What it would take | What ships instead |
|
||||
| --- | --- | --- | --- |
|
||||
| `ACCESS` column and `your access → CONNECT-ONLY` | server | Per-item ACLs. `EncryptedPayload.DataKeyId` is documented as the seam for per-item grants **in M5**. `VaultSummary` does carry an opaque `int Permissions`, but nothing gives the bits a meaning on the wire — `PermissionFlags` itself lives in `DodoSSH.Domain` — and those are per *vault*, never per item. There is no `ConnectOnly` role in `TeamRole` at all. | Omitted. The column shows sync state instead — whether a change is still sitting in this machine's outbox — which the design had no column for. |
|
||||
| `SHARED WITH · 6` avatars | server | **Built in M3**, minus the avatars — no picture is stored anywhere. `GET /api/v1/vaults/{id}/grants` lists who holds a key. | The list lives on the TEAMS screen, beside the members it is about, rather than as a count on an item row: a grant is per *vault*, and putting a number on an item would imply per-item sharing, which is M5. |
|
||||
| `SHARED WITH · 6` avatars | server | **Built in M3**, minus the avatars — no picture is stored anywhere. `GET /api/v1/vaults/{id}/grants` lists who holds a key. | The list lives on the VAULTS screen, beside the members it is about, rather than as a count on an item row: a grant is per *vault*, and putting a number on an item would imply per-item sharing, which is M5. |
|
||||
| "Private key never leaves the vault. Sessions sign through the team agent (dodod)" | client-ssh | **Both sentences are false here, and the second cannot be made true by this architecture.** There is no agent of any kind, and the connect path decrypts the private key and hands the bytes to SSH.NET. | Omitted. The key editor already says what is true: the key and its passphrase are encrypted here and never reach the server in a readable form. |
|
||||
| Scope rail: `PERSONAL` / `TEAM · PLATFORM` / `TEAM · DATA` | server | **Built in M3.** `VaultAccessService.ListAsync` resolves team membership, so a session can hold several vaults. | Not a rail, because it would be a selector with nothing to select: every list on the screen already spans every vault this session can read. What replaces it is a picker for where a *new* item is filed, which is the only vault question with an answer. |
|
||||
| `SCOPE` column | server | **Built in M3.** Scope is a property of a vault, never of an item, and that has not changed. | Each row names the vault it is in, and rows are grouped by vault. |
|
||||
@@ -379,20 +381,30 @@ and both editors. What follows is what the design drew around them.
|
||||
| `added by anna@dodotech.dev` | contracts | The server records `CreatedByUserId`, but `SyncChange` carries no actor field and no other user's name is fetchable. | Omitted, and the detail pane says in one line that items record no author, no timestamps and no sharing. |
|
||||
| `created 2026-03-14` | client-sync | Recoverable in principle — entity ids are UUIDv7 and carry a timestamp — but nothing surfaces it. | Omitted. |
|
||||
| `TEST CONNECT` | ui | Connecting is host-scoped, not credential-scoped: there is nothing to test a credential *against* without a host. | Omitted. |
|
||||
| `REVOKE` | server | **Built in M3**, as WITHDRAW KEY on the TEAMS screen — because there are now grants to revoke, and a grant is what it acts on. ADR 0001 constrains how it is presented, and it is: the message says it blocks future reads only, and that what they already hold is unaffected. | On this screen, still Delete, named for what it does. Deleting an item and withdrawing somebody's key remain different acts. |
|
||||
| `REVOKE` | server | **Built in M3**, as WITHDRAW KEY on the VAULTS screen — because there are now grants to revoke, and a grant is what it acts on. ADR 0001 constrains how it is presented, and it is: the message says it blocks future reads only, and that what they already hold is unaffected. | On this screen, still Delete, named for what it does. Deleting an item and withdrawing somebody's key remain different acts. |
|
||||
| `SSH KEY · ED25519` | client-domain | No algorithm field, and deriving it means parsing the armour. | The type without the algorithm. |
|
||||
| One `+ ADD CREDENTIAL` button | ui | — | Two buttons, one per kind that can be added. "Credential" is a specific item type in this codebase (a username and a password), so using it as an umbrella word would collide with the vocabulary. |
|
||||
| — | — | — | **`HOST KEYS` is the reverse case:** a fully-backed, shipped category the design had no slot for. It is in the rail. |
|
||||
|
||||
---
|
||||
|
||||
## Teams
|
||||
## Vaults
|
||||
|
||||
**Built in M3.** The screen ships: a team list, a members table with a real last-active column, the
|
||||
invitations standing against addresses that have no account here yet, the team's vaults, and the two buttons
|
||||
the whole design was really about — add a member, and share a vault key. A team can also be renamed, handed
|
||||
to another member, and archived, the last only while it owns no vaults. What follows is what it still does
|
||||
not do, and one thing this document got wrong before it was built.
|
||||
**Built in M3, and reshaped since.** The screen ships: a vault list, a members table with a real
|
||||
last-active column, the invitations standing against addresses that have no account here yet, who holds a
|
||||
key, and the two buttons the whole design was really about — add somebody, and share a vault key. A vault
|
||||
can also be renamed and handed to another member.
|
||||
|
||||
**It lists vaults where it used to list teams, and that is the reshaping.** A team is still what the server
|
||||
authorises against; what went is the requirement that anybody make one. Naming a vault makes the membership
|
||||
list that carries it, named after the vault and owned by its creator, so the thing people came to share is
|
||||
the thing they create. The team resurfaces in exactly one place and is not hidden there: a membership list
|
||||
carrying several vaults — which this screen cannot produce — says so, because adding somebody to one of
|
||||
those vaults adds them to all of them. Archiving is gone with the team list: a vault cannot be deleted at
|
||||
all, the server refuses to archive a membership list while its vault exists, so the screen says so rather
|
||||
than offering a button that always refuses.
|
||||
|
||||
What follows is what it still does not do, and one thing this document got wrong before it was built.
|
||||
|
||||
**The correction.** The rows below used to describe a screen with nothing behind it, on the grounds that
|
||||
`VaultAccessService.ResolveAsync` denied every vault that was not the caller's own. That is now the one
|
||||
|
||||
+47
-43
@@ -28,7 +28,7 @@ a phase had nothing left for a person to do, which is the good outcome rather th
|
||||
|
||||
### 1.1 No screen is sliced at the WebView's left edge · **the important one**
|
||||
|
||||
Open two terminals, then visit every nav rail entry in turn — Hosts, Keychain, Pins, Snippets, Logs, Teams,
|
||||
Open two terminals, then visit every nav rail entry in turn — Hosts, Keychain, Pins, Snippets, Logs, Vaults,
|
||||
Preferences — and both of the fixed tabs, SFTP and S3.
|
||||
|
||||
**Pass:** each screen draws whole, its buttons all clickable, and the tab strip stays across the top of all
|
||||
@@ -1130,7 +1130,7 @@ case, and those two have to move together — the switch mirrors that property b
|
||||
|
||||
---
|
||||
|
||||
## Phase 12 — Teams: the operations that span two accounts
|
||||
## Phase 12 — Shared vaults: the operations that span two accounts
|
||||
|
||||
The server's own rules are covered by the endpoint suite: teams are renamed, an archive is refused while a
|
||||
vault is in the way, ownership changes hands, and every branch of the invitation claim is driven with
|
||||
@@ -1147,16 +1147,16 @@ follows is about what happens the first time it does.
|
||||
|
||||
### 12.1 An invitation becomes a membership at the invitee's first sign-in · **the one worth the most care**
|
||||
|
||||
1. Sign in as `alice`, make a team, and open its invitations.
|
||||
2. Invite `bob@example.com` as a Member. **Nothing is sent, and nothing should look as though it was** —
|
||||
1. Sign in as `alice`, make a vault on the VAULTS screen, and select it.
|
||||
2. Add `bob@example.com` as a Member. **Nothing is sent, and nothing should look as though it was** —
|
||||
no "invitation emailed", no link to copy, no token anywhere on the screen.
|
||||
3. **Pass:** the row appears as *pending*, carrying the address, the role and an expiry fourteen days out.
|
||||
Bob is **not** in the members table, because he has no account here for a membership row to point at.
|
||||
4. Sign in as `bob` on the second profile and enroll.
|
||||
5. **Pass:** the team is in Bob's list the first time he looks, at Member, with nothing further pressed on
|
||||
either side. Back on Alice's machine, refresh: the invitation reads *accepted* rather than vanishing,
|
||||
5. **Pass:** the vault is in Bob's list the first time he looks, at Member, with nothing further pressed
|
||||
on either side. Back on Alice's machine, refresh: the invitation reads *accepted* rather than vanishing,
|
||||
and Bob is now in the members table.
|
||||
6. **Pass, and this is the half that is easiest to lose:** the team's vault is in Bob's list **saying it is
|
||||
6. **Pass, and this is the half that is easiest to lose:** the vault is in Bob's list **saying it is
|
||||
waiting for a key**, and nothing in it is readable. Have Alice press SHARE KEY and Bob sync; now it
|
||||
opens.
|
||||
|
||||
@@ -1171,15 +1171,15 @@ reached that machine by a route this architecture says does not exist.
|
||||
In Keycloak's admin console, clear **Email verified** on the invitee *before* their first DodoSSH sign-in.
|
||||
Invite that address, then sign in as them.
|
||||
|
||||
**Pass:** they get an account and a personal vault and no team at all. The invitation stays *pending* on
|
||||
**Pass:** they get an account and a personal vault and no shared one. The invitation stays *pending* on
|
||||
the inviter's screen rather than turning into anything, and the API log carries a warning naming how many
|
||||
invitations it declined to claim. Now set **Email verified** back on. The claim happens on the next request
|
||||
that crosses the hourly last-seen window, so it is **not** immediate and restarting the client will not
|
||||
hurry it along — the account already exists, so there is no second first-sign-in to trigger it.
|
||||
|
||||
**Failure means:** if the team appears while the address is unverified, the one security boundary
|
||||
**Failure means:** if the vault appears while the address is unverified, the one security boundary
|
||||
invitations have is not being enforced, and anybody able to obtain a token asserting a colleague's address
|
||||
can walk into their team. Stop there. If it stays pending after verifying, the claim is not reaching the
|
||||
can walk into their vault. Stop there. If it stays pending after verifying, the claim is not reaching the
|
||||
**access** token — check the provider's mappers, and set `Oidc:EmailVerifiedClaim` if it sends the claim
|
||||
under some other name.
|
||||
|
||||
@@ -1188,27 +1188,27 @@ under some other name.
|
||||
Invite an address, then revoke it before anybody has signed in with it. Then sign in with that address.
|
||||
|
||||
**Pass:** the row reads *revoked* and stays on the list rather than disappearing, and the sign-in produces
|
||||
an ordinary account in no team. Revoking one that has *already* been accepted answers that there was
|
||||
nothing to withdraw.
|
||||
an ordinary account in no shared vault. Revoking one that has *already* been accepted answers that there
|
||||
was nothing to withdraw.
|
||||
|
||||
**Failure means:** a revoked invitation that still lets somebody in is a removal that did not remove. An
|
||||
accepted one that could be unpicked here would be worse: it is a membership now, and removing a member
|
||||
revokes their vault key grants and flags every team vault for rekey, which is not what "revoke invitation"
|
||||
revokes their vault key grants and flags the vault for rekey, which is not what "revoke invitation"
|
||||
should quietly do.
|
||||
|
||||
### 12.4 An address already in the team is refused; an address that merely has an account is not
|
||||
|
||||
With Bob in the team, invite `bob@example.com` to it again.
|
||||
With Bob in the vault, add `bob@example.com` to it again.
|
||||
|
||||
**Pass:** refused, with a sentence saying the address already belongs to a member and to change their role
|
||||
instead. Now make a **second** team and invite the same address there.
|
||||
instead. Now make a **second** vault and invite the same address there.
|
||||
|
||||
**Pass:** accepted. Bob having an account is deliberately not a reason to refuse — it is claimed within the
|
||||
hour on his next request rather than at a sign-in, so give it that long before deciding it has not worked.
|
||||
|
||||
**Failure means:** if the second invitation is refused because the address already has an account, this
|
||||
endpoint has become a way of asking the server which addresses have accounts on it, answerable by anybody
|
||||
willing to create a team first. See ADR 0009.
|
||||
willing to create a vault first. See ADR 0009.
|
||||
|
||||
### 12.5 LAST ACTIVE is a real time, and a coarse one · **needs a couple of hours**
|
||||
|
||||
@@ -1227,49 +1227,53 @@ impossible to offer honestly before.
|
||||
|
||||
As the owner, transfer ownership to another active member, then read both rows.
|
||||
|
||||
**Pass:** they are Owner and you are **Admin** — not removed, not Member. Your vault key grants are intact
|
||||
and the team's vaults have not come back flagged for rekey. Then try to transfer to somebody who is not a
|
||||
member, and to yourself.
|
||||
**Pass:** they are Owner and you are **Admin** — not removed, not Member. Your vault key grant is intact
|
||||
and the vault has not come back flagged for rekey. Then try to hand it to somebody who is not a member, and
|
||||
to yourself.
|
||||
|
||||
**Pass:** both refused, and the message says which.
|
||||
|
||||
**Failure means:** two owners, or none, is the state this being a single transaction exists to prevent, and
|
||||
either one leaves a team that no client can administer back into shape. If your grants were revoked or the
|
||||
vaults are now flagged for rekey, the transfer is removing the outgoing owner rather than demoting them.
|
||||
either one leaves a vault that no client can administer back into shape. If your grant was revoked or the
|
||||
vault is now flagged for rekey, the transfer is removing the outgoing owner rather than demoting them.
|
||||
|
||||
### 12.7 Archiving is refused while the team owns a vault
|
||||
### 12.7 A vault cannot be deleted, and the screen says so rather than offering a button
|
||||
|
||||
With a team that owns at least one vault, try to archive it.
|
||||
Look for a way to remove a vault, on both heads.
|
||||
|
||||
**Pass:** refused, and the message counts the vaults in the way and says there is no way to delete a vault
|
||||
in this product. The team is still in everybody's list afterwards and its vaults still open.
|
||||
**Pass:** there is none, and the VAULTS screen says why in a sentence: nothing in this product removes a
|
||||
vault, and the server refuses to archive the membership list behind one while it exists. Archiving that
|
||||
list is still reachable over the API, and the endpoint suite drives both its refusal and its success — what
|
||||
is being checked here is that no button offers it.
|
||||
|
||||
**Failure means:** an archive that succeeded here would have taken those vaults out of the list of
|
||||
everybody holding a key — including the person who pressed it, quietly, and with nothing in the product
|
||||
able to put them back.
|
||||
**Failure means:** a delete that worked would take the vault out of the list of everybody holding a key —
|
||||
including the person who pressed it, quietly, and with nothing in the product able to put them back. A
|
||||
button that always refuses is the milder failure and is still worth removing.
|
||||
|
||||
### 12.8 Archiving an empty team takes its memberships and its invitations with it · **needs two accounts**
|
||||
### 12.8 Renaming a vault reaches every place its name is drawn · **needs two accounts**
|
||||
|
||||
Make a team that owns no vaults, add the second account to it, invite a third address, and archive it.
|
||||
Rename a shared vault from the VAULTS screen.
|
||||
|
||||
**Pass:** the team is gone from both accounts' lists. Sign in with the invited address afterwards and it
|
||||
joins nothing. A new team can be created under the archived one's slug.
|
||||
**Pass:** the new name is on the vault list, on the badge of every host card in that vault, in the keychain
|
||||
screen's "new items go to" picker, in the host editor's vault picker, and in the tab strip's vault menu —
|
||||
and on the second account after a refresh. Nothing in the vault needs re-encrypting and everybody's key
|
||||
still opens it.
|
||||
|
||||
**Failure means:** the invited address turning up in a team nobody can see is exactly what revoking pending
|
||||
invitations inside the same transaction exists to prevent, and it would happen weeks later on a sign-in
|
||||
nobody is watching. Note that taking the freed slug is correct rather than a defect, and is also the reason
|
||||
an archived team is only restorable by an operator who checks that first.
|
||||
**Failure means:** a name that moved in one place and not another is the shape this rename is most likely to
|
||||
fail in, because several screens read it separately from a cached vault row. A vault that stops opening
|
||||
after a rename would be far worse, and means that cached row was replaced by the server's answer rather
|
||||
than edited — that answer deliberately carries no wrapped key.
|
||||
|
||||
### 12.9 Renaming a team, and the slug that does not move
|
||||
### 12.9 Somebody who may write to a vault may not rename it
|
||||
|
||||
Rename a team and change its description.
|
||||
As a plain Member of somebody else's vault, look for RENAME.
|
||||
|
||||
**Pass:** the new name is on every screen that names the team, on both accounts after a refresh. The slug is
|
||||
unchanged and there is nowhere to change it. Nothing claims to know *when* it was renamed.
|
||||
**Pass:** it is not drawn. Adding a host to that vault still works, which is what makes this a boundary
|
||||
rather than a broken role.
|
||||
|
||||
**Failure means:** a rename that moved the slug could take one an archived team is still holding, and that
|
||||
archived team could then never be brought back. An "edited" timestamp anywhere on the screen is invented
|
||||
data — `team` has no updated-at column, so there is nothing behind it.
|
||||
**Failure means:** a name is what everybody in the vault sees it called, so a member renaming it out from
|
||||
under the people who share it is an administrative act reached without the role for it. The server refuses
|
||||
it too — this is the interface not offering what the server would turn down.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user