Public Access
Let a team be joined only by somebody who is already here
An invitation decided access from an assertion about an address. Everything else
in this model decides it from something a person did — an admin naming an
account, a key holder wrapping a vault key to a key they verified — and this was
the one place a token's email claim was the thing that let somebody in.
It was guarded as tightly as that can be guarded: the claim was refused outright
on an unverified or absent `email_verified`, with no setting to relax it. But the
guard and the risk were the same shape. The whole defence was one boolean sent by
a system the deployment does not control.
So `POST /teams/{id}/members` is the only way in, and an address with no account
is refused with `no-such-account` — which is now the end of the road rather than
the signal to invite. Both clients say the remedy: that person signs in here
once, which is what creates the account, and then they can be added. The desktop
leaves the address in the box, because a message telling you to come back later
is one you act on later.
Gone with it: the `team_invitation` table, the claim hook in the sign-in path,
and `Oidc:EmailVerifiedClaim`, which that hook was the only reader of. Nothing in
the server now reads the email claim to decide anything.
Pending invitations are dropped rather than converted. Converting one would mean
creating a membership because an address matched, which is the property being
removed — and an invitation to an address that did have an account here had
already been claimed by the hourly sweep, so what is left is offers to people who
never arrived.
Two tests carry the property rather than the feature: the endpoint inventory
asserts the three routes are absent, and the API suite adds an address that has
no account, watches the refusal, then signs that address in and checks it joined
nothing. Without the second half, a server that merely renamed the deferred path
would pass.
This commit is contained in:
@@ -10,14 +10,11 @@
|
||||
VAULTS, under MORE — and the one screen behind that hub the v2 phone design never drew.
|
||||
|
||||
It is the reverse of every other entry in docs/design-import-gaps.md: a shipped screen the design had
|
||||
no slot for, rather than a drawn screen with nothing behind it. It is on the phone because 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 vault the server has just put somebody into, visible only
|
||||
on a head they may never have installed, is a membership they cannot see.
|
||||
|
||||
That argument is also why the invited list is drawn here and not treated as an administrator's detail:
|
||||
the people on it are the ones who cannot yet see the vault, and the row says out loud that no mail was
|
||||
sent.
|
||||
no slot for, rather than a drawn screen with nothing behind it. It is on the phone because a vault
|
||||
arrives without being asked for — somebody wraps its key to you from their machine — and the person it
|
||||
arrives for is at least as likely to be holding a phone as sitting at a desktop. A vault the server has
|
||||
just put somebody into, visible only on a head they may never have installed, is a membership they
|
||||
cannot see.
|
||||
|
||||
── IT LISTED TEAMS UNTIL THE SCREEN STOPPED BEING ABOUT THEM. ───────────────────────────────────────
|
||||
The rows are vaults now, and the members under one are the people that vault is shared with. Nothing
|
||||
@@ -39,20 +36,20 @@
|
||||
◆ **SHARE KEY is drawn and nothing that takes something away is.** That is a decision rather than a
|
||||
subset. Wrapping a vault key is the one act on this screen a server cannot perform at all — it needs a
|
||||
machine that already holds the key, and this phone is one — so a vaults screen that could only be read
|
||||
would leave the product's central claim undemonstrated on the head most people carry. REMOVE, WITHDRAW
|
||||
KEY and WITHDRAW INVITATION are the other half of that, and each of them acts on the first press: the
|
||||
view model's armed-confirmation state covers handing a vault over and nothing else. The desktop guards
|
||||
would leave the product's central claim undemonstrated on the head most people carry. REMOVE and
|
||||
WITHDRAW KEY are the other half of that, and both act on the first press: the view model's
|
||||
armed-confirmation state covers handing a vault over and nothing else. The desktop guards
|
||||
them with a tooltip instead, which is a control a touch screen has no way to show. An irreversible
|
||||
revocation under a thumb with its explanation missing is the wrong trade, so all three stay on the
|
||||
revocation under a thumb with its explanation missing is the wrong trade, so both stay on the
|
||||
desktop — where the sentence beside them is visible. Handing a vault over is not drawn either, for a
|
||||
plainer reason: it decides who controls the vault, which is not a thing to do while walking. Nor is
|
||||
renaming, which is a keyboard on a screen that is otherwise all reading.
|
||||
|
||||
**ADD is not drawn either**, and it is the operation this screen least needs. It is an address typed
|
||||
into a box, a directory lookup, a role picker, and a paragraph beside it saying what adding somebody
|
||||
did *not* do — and since invitations arrived the ordinary way into a vault is one the server claims at
|
||||
sign-in, which is what put this screen on the phone at all. Making a vault is here, because it is one
|
||||
field and because it is what a person carrying a phone can usefully start.
|
||||
did *not* do — five controls for the one act on this screen that a colleague at a desktop is already
|
||||
doing. Making a vault is here, because it is one field and because it is what a person carrying a
|
||||
phone can usefully start.
|
||||
|
||||
**The key-holder list is not drawn.** It is a third list, and what the phone can answer about a vault
|
||||
is the more useful half of the same question and is on the vault's own row: whether *this* machine can
|
||||
@@ -61,7 +58,7 @@
|
||||
**↻ and `+` both, because this screen has more reason to re-read than any other.** Who is in a vault is
|
||||
not cached — it is read from the server on arrival and again at the end of every command — so the one
|
||||
thing a member cannot otherwise see is a change somebody else just made: a vault key wrapped to them
|
||||
from a colleague's desktop, or a vault they have this moment been invited into. On the desktop the
|
||||
from a colleague's desktop, or a vault they have this moment been added to. On the desktop the
|
||||
re-read is leaving the rail and coming back, which is one click. Here it is a trip out to MORE and
|
||||
back, so the button earns its place. It binds to a real command rather than to ShowScreen(Vaults),
|
||||
which would set Screen to the value it already holds, raise nothing and reload nothing.
|
||||
@@ -246,53 +243,6 @@
|
||||
<TextBlock Classes="body" Margin="18,10,18,0"
|
||||
Text="Being in a vault is what lets the server hand somebody its rows. It is not what lets them read one: a vault key can only be wrapped by a machine that already holds it, which is what sharing below does." />
|
||||
|
||||
<!-- ============ ◆ who has been asked and has not arrived ============ -->
|
||||
<!--
|
||||
The section this screen exists for, and the one the desktop had nothing to draw until
|
||||
invitations were built. Read-only here: withdrawing one is a control that acts on the first
|
||||
press, which is the line drawn at the top of this file.
|
||||
|
||||
So these are cards rather than the flat rows above them, and the shape is the difference: a
|
||||
row that fills when you touch it is one of several you are choosing between, and there is
|
||||
nothing to choose here. An ItemsControl rather than a ListBox for the same reason — a list
|
||||
with a selection nothing reads would be a control offering something it cannot do.
|
||||
|
||||
Gated on the view model's own count rather than left to stand over an empty list, because a
|
||||
vault with nobody outstanding is the ordinary case and a permanent empty heading would make
|
||||
it look like a section that had failed to load.
|
||||
|
||||
The waiting row carries the whole mechanism in its own sentence — no mail was sent, and they
|
||||
join when they first sign in here. That is the sentence somebody has to read, because every
|
||||
other product's version of this word means an email is on its way.
|
||||
-->
|
||||
<StackPanel IsVisible="{Binding HasInvitations}">
|
||||
<TextBlock Classes="section" Text="INVITED" Margin="18,18,18,4" />
|
||||
|
||||
<ItemsControl ItemsSource="{Binding Invitations}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:VaultInvitationRowViewModel">
|
||||
<Border Classes="card" Margin="12,3">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="12.5" Text="{Binding Email}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="detail" FontSize="10" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource TextDim}" Text="{Binding State}"
|
||||
IsVisible="{Binding !IsPending}" />
|
||||
<TextBlock Classes="detail" FontSize="10" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource WarnText}" Text="{Binding State}"
|
||||
IsVisible="{Binding IsPending}" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="1" Classes="tag outline" Margin="8,0,0,0">
|
||||
<TextBlock Text="{Binding Role}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
Reference in New Issue
Block a user