Public Access
Connections: drop the manual box's view-model state now that both heads ask through the prompt
The phone's Connections screen no longer draws the user@host + password box, and the desktop never did, so nothing bound VaultViewModel.ManualTarget, ManualPassword, ManualStatus or ConnectManuallyCommand any more, nor MainWindowViewModel.ConnectToRecentCommand, whose address branch wrote into that box. They are gone, and so is VaultViewModel.AskAboutHost, whose one caller was ConnectToRecent. TryParseManualTarget, ManualEndpoint and ConnectToEndpointAsync stay: the prompt, the desktop's Ctrl+K palette and the phone's hosts search all use them. The reasoning the removed members carried moved onto ConnectToEndpointAsync and ChoosePromptRecentAsync. The tests whose subject is still live now go through the prompt instead of the box: a typed address dialled exactly as typed, port 22 by default, the refusal sentences, the host-key retry replaying the attempt that raised it, the host-key card staying over the surface it was typed on, the session facts for a typed password, and no forwards for a typed address. The recent-row cases now drive ChoosePromptRecentCommand. Two cases existing prompt tests already covered were deleted. Manual check 11.8 now describes recent rows landing on the prompt.
This commit is contained in:
1 parent
0cecf1afc4
commit
fa85ad1eff
7 files changed
+142
-313
No files matched your search
@@ -170,7 +170,7 @@
|
||||
The second step, in the same card, so the keyboard never leaves the palette: Enter on the address
|
||||
comes here, Enter here dials, Escape goes back to the address rather than closing. The box carries
|
||||
PasswordChar and nothing is written anywhere — see MainWindowViewModel.IsAskingForAdHocPassword,
|
||||
and VaultViewModel.ConnectManuallyAsync for why a typed password is the only way in.
|
||||
and VaultViewModel.ConnectToEndpointAsync for why a typed password is the only way in.
|
||||
-->
|
||||
<StackPanel x:Name="PasswordStep" IsVisible="{Binding IsAskingForAdHocPassword}">
|
||||
|
||||
|
||||
@@ -68,9 +68,9 @@ internal sealed class ConnectionLogRowViewModel(VaultItem<ConnectionLogSecret> e
|
||||
/// <summary>The keychain host this was, if it was one.</summary>
|
||||
/// <remarks>
|
||||
/// Null for a connection made to a typed address, and that is a real distinction rather than missing
|
||||
/// data — see <c>VaultViewModel.ConnectManuallyAsync</c>. It is what lets the Connections screen offer
|
||||
/// the right thing when one of these rows is tapped: a keychain host has a connect bar with its own
|
||||
/// authentication behind it, and an address has only the box it was typed into.
|
||||
/// data — see <c>VaultViewModel.ConnectToEndpointAsync</c>. It is what lets the connect prompt offer the
|
||||
/// right thing when one of these rows is tapped: a keychain host has its own authentication behind it, and
|
||||
/// an address has only the password typed for it — see <c>MainWindowViewModel.ChoosePromptRecentAsync</c>.
|
||||
/// </remarks>
|
||||
internal Guid? HostId => entry.Secret.HostId;
|
||||
|
||||
|
||||
@@ -1857,9 +1857,9 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
/// button needs a command.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Not gated on there being a tab. With nothing open the surface is where a connection is made — the
|
||||
/// phone's terminal screen draws its manual box there, and the desktop its prompt; see
|
||||
/// <see cref="IsConnectPromptShowing"/> — which is worth being able to reach deliberately.
|
||||
/// Not gated on there being a tab. With nothing open the surface is where a connection is made — both
|
||||
/// heads draw the connect prompt there; see <see cref="IsConnectPromptShowing"/> — which is worth being
|
||||
/// able to reach deliberately.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RelayCommand]
|
||||
@@ -1946,57 +1946,6 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
OnPropertyChanged(nameof(HasRecentConnections));
|
||||
}
|
||||
|
||||
/// <summary>Goes back to a machine that has been connected to before.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Two destinations, because a recent row is one of two different things.</b> One that names a
|
||||
/// keychain host goes to that host on the hosts screen, with whatever that head uses to ask about one
|
||||
/// machine raised over it — the desktop's drawer, the phone's action bar — carrying whatever
|
||||
/// authentication the keychain resolves for it. Connecting from here instead would be a third connect
|
||||
/// path that had to answer all of that again.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// ◆ <b>It raises that rather than merely selecting the row, and on the phone it has to.</b> Nothing on
|
||||
/// that list means "selected" any more — a tap connects and a long press ticks — so arriving with the
|
||||
/// host selected and nothing else would be arriving at a screen with nothing to press. Asking to go back
|
||||
/// to a machine is exactly the deliberate act those flags exist to distinguish from browsing. See
|
||||
/// <c>VaultViewModel.AskAboutHost</c>, which is where the two heads' answers are raised together.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// One that names no item was typed into the manual box, and the log stored exactly what was dialled —
|
||||
/// <c>user@host:port</c>, which is the grammar that box takes. So it goes back into the box, and what
|
||||
/// is deliberately not restored is the password: it was never stored, which is the whole point of the
|
||||
/// manual path, and a field that filled itself in would be claiming otherwise.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A host deleted since it was connected to falls through to the address, which is the honest answer:
|
||||
/// the machine is still there and the keychain no longer knows about it. So does a host in a vault the
|
||||
/// user has switched off, and for the same reason rather than by accident: selecting it would point the
|
||||
/// hosts screen at a row that screen is not drawing, and the grid would null the selection straight back
|
||||
/// out — arriving at the hosts screen with nothing selected and no explanation.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RelayCommand]
|
||||
private void ConnectToRecent(ConnectionLogRowViewModel row)
|
||||
{
|
||||
if (row is null || Vault is not { } vault)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (row.HostId is { } hostId
|
||||
&& vault.Hosts.FirstOrDefault(host => host.EntityId == hostId) is { } known
|
||||
&& vault.IsVaultShown(known.VaultId))
|
||||
{
|
||||
vault.AskAboutHostCommand.Execute(known);
|
||||
ShowScreen(ShellScreen.Hosts);
|
||||
return;
|
||||
}
|
||||
|
||||
vault.ManualTarget = row.Address;
|
||||
vault.ManualStatus = string.Empty;
|
||||
}
|
||||
|
||||
// ---- The rail's own page grouping ----
|
||||
|
||||
/// <summary>
|
||||
@@ -2602,10 +2551,20 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
/// A click on one of the recent connections printed under the prompt.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A row naming a host still in the keychain goes to that host, with whatever the keychain knows about
|
||||
/// it; one that names none — or a host deleted since — is the address that was dialled, and asks for its
|
||||
/// password the way a typed one does. The same split <see cref="ConnectToRecent"/> makes, and for its
|
||||
/// reason, except that here the answer is a line of the prompt rather than another screen.
|
||||
/// <para>
|
||||
/// <b>Two answers, because a recent row is one of two different things.</b> A row naming a host still in
|
||||
/// the keychain goes to that host, with whatever the keychain knows about it — its key, its saved
|
||||
/// password, its refusals — exactly as typing its name would. One that names none was typed in as an
|
||||
/// address, and the log stored exactly what was dialled, <c>user@host:port</c>, which is the grammar the
|
||||
/// prompt takes. So it goes back onto the prompt and asks for its password the way a typed one does; the
|
||||
/// password was never stored, which is the whole point of that path.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A host deleted since it was connected to falls through to the address, which is the honest answer:
|
||||
/// the machine is still there and the keychain no longer knows about it. So does a host in a vault the
|
||||
/// user has switched off, and for the same reason rather than by accident: taking it would reach a host
|
||||
/// the rest of the application is deliberately not showing.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RelayCommand(AllowConcurrentExecutions = true)]
|
||||
private async Task ChoosePromptRecentAsync(ConnectionLogRowViewModel? row)
|
||||
@@ -2807,13 +2766,13 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
/// <para>
|
||||
/// <b>The palette's answer to a machine that is not in the keychain.</b> Every other row it offers is a
|
||||
/// saved host, and a box somebody has just been given the address of had no way in on this head short of
|
||||
/// saving it first — the phone has had a manual box on its terminal screen all along. The query already
|
||||
/// is a box, and <c>user@host</c> is the grammar people type into one, so the palette reads it rather
|
||||
/// than growing a second field.
|
||||
/// saving it first. The query already is a box, and <c>user@host</c> is the grammar people type into
|
||||
/// one, so the palette reads it rather than growing a second field.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Read with <see cref="VaultViewModel.TryParseManualTarget"/>, which is the phone's box's own parser, so
|
||||
/// the two heads accept exactly the same addresses and refuse the rest with the same sentences.
|
||||
/// Read with <see cref="VaultViewModel.TryParseManualTarget"/>, which is the connect prompt's parser and
|
||||
/// the phone's hosts search's too, so every box accepts exactly the same addresses and refuses the rest
|
||||
/// with the same sentences.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Only a query with an <c>@</c> in it is read at all. Without one, everything typed is a search, and a
|
||||
@@ -2884,7 +2843,7 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
|
||||
/// <summary>The password typed for <see cref="AdHocTarget"/>.</summary>
|
||||
/// <remarks>
|
||||
/// Cleared whenever the palette closes or goes back to the query, on the same terms the phone's manual box
|
||||
/// Cleared whenever the palette closes or goes back to the query, on the same terms the connect prompt
|
||||
/// clears its own: it is a secret nobody asked to keep, and the next address typed must not arrive with the
|
||||
/// last one's password already in the box.
|
||||
/// </remarks>
|
||||
@@ -3001,8 +2960,8 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Through <see cref="VaultViewModel.ConnectToEndpointAsync"/>, which is the phone's manual box's own
|
||||
/// path, so a machine reached from here gets the same tab, the same host-key question and the same
|
||||
/// Through <see cref="VaultViewModel.ConnectToEndpointAsync"/>, which is the connect prompt's own path,
|
||||
/// so a machine reached from here gets the same tab, the same host-key question and the same
|
||||
/// connection-log entry. What is written, if the user answers the host-key question, is a pin — the trust
|
||||
/// decision belongs to the endpoint rather than to a keychain item, and a machine reached this way is
|
||||
/// exactly the one whose key nobody has seen before.
|
||||
|
||||
@@ -4251,48 +4251,21 @@ internal sealed partial class VaultViewModel(
|
||||
[NotifyPropertyChangedFor(nameof(ShowsAddButton))]
|
||||
private bool isAskingForConnectPassword;
|
||||
|
||||
/// <summary>What was typed into the manual connect box, as <c>user@host</c> or <c>user@host:port</c>.</summary>
|
||||
/// <remarks>
|
||||
/// One box rather than four, because this is the form of an address people already have: it is what a
|
||||
/// colleague pastes into a chat window and what `ssh` itself takes. Splitting it into user, host and port
|
||||
/// would make the ordinary case three taps between three keyboards on a phone.
|
||||
/// </remarks>
|
||||
[ObservableProperty]
|
||||
private string manualTarget = string.Empty;
|
||||
|
||||
/// <inheritdoc cref="ConnectPassword" />
|
||||
/// <remarks>
|
||||
/// Its own box rather than <see cref="ConnectPassword"/>, for the reason <c>TryBuildConnectionRequest</c>
|
||||
/// takes the typed password as a parameter: these are different screens, and a password typed on one is
|
||||
/// not a password offered on the other.
|
||||
/// </remarks>
|
||||
[ObservableProperty]
|
||||
private string manualPassword = string.Empty;
|
||||
|
||||
/// <summary>Why the manual box refused, if it did.</summary>
|
||||
/// <remarks>
|
||||
/// Beside that box rather than only on <see cref="Status"/>. The refusals here are about what was typed
|
||||
/// — a missing account, a port that is not a number — and a sentence about a text box belongs next to
|
||||
/// the text box, not on a status line that also carries what the sync engine is doing.
|
||||
/// </remarks>
|
||||
[ObservableProperty]
|
||||
private string manualStatus = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The machine the hosts search box names, when what was typed into it reads as <c>user@host[:port]</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>The phone's version of the desktop palette's typed-address row.</b> The manual box on the terminal
|
||||
/// screen is the other way in, and it is only drawn while nothing is open — a second machine reached while
|
||||
/// a first one's session is up had no box at all. The search well at the top of HOSTS is always there, the
|
||||
/// design already calls it "Search or connect…", and <c>user@host</c> is what people type into a box, so
|
||||
/// it reads the address rather than the screen growing a second field.
|
||||
/// <b>The phone's version of the desktop palette's typed-address row.</b> The connect prompt on the
|
||||
/// terminal screen is the other way in, and it is only drawn while nothing is open — a second machine
|
||||
/// reached while a first one's session is up had no box at all. The search well at the top of HOSTS is
|
||||
/// always there, the design already calls it "Search or connect…", and <c>user@host</c> is what people
|
||||
/// type into a box, so it reads the address rather than the screen growing a second field.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Read with <see cref="TryParseManualTarget"/>, the manual box's own parser, and only for a filter with an
|
||||
/// <c>@</c> in it: without one, everything typed is a search, and a card offering to dial three letters
|
||||
/// would arrive on every keystroke.
|
||||
/// Read with <see cref="TryParseManualTarget"/>, the prompt's and the palette's parser too, and only for a
|
||||
/// filter with an <c>@</c> in it: without one, everything typed is a search, and a card offering to dial
|
||||
/// three letters would arrive on every keystroke.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[ObservableProperty]
|
||||
@@ -4318,10 +4291,9 @@ internal sealed partial class VaultViewModel(
|
||||
|
||||
/// <summary>The password typed for <see cref="FilterAddress"/>.</summary>
|
||||
/// <remarks>
|
||||
/// Its own box rather than <see cref="ManualPassword"/>, for the reason that one is not
|
||||
/// <see cref="ConnectPassword"/>: these are different screens, and a password typed on one is not a
|
||||
/// password offered on the other. Cleared when the address changes, so the next machine typed never
|
||||
/// arrives with the last one's password already in the box.
|
||||
/// Its own box rather than <see cref="ConnectPassword"/>: these are different screens, and a password
|
||||
/// typed on one is not a password offered on the other. Cleared when the address changes, so the next
|
||||
/// machine typed never arrives with the last one's password already in the box.
|
||||
/// </remarks>
|
||||
[ObservableProperty]
|
||||
private string filterAddressPassword = string.Empty;
|
||||
@@ -7820,40 +7792,6 @@ internal sealed partial class VaultViewModel(
|
||||
IsHostPaneOpen = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises whatever the head in front of the user uses to ask about one host.
|
||||
/// </summary>
|
||||
/// <param name="row">The machine, which the caller is holding.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// ◆ <b>One command, two pieces of furniture, and it exists because the phone's has changed.</b> The
|
||||
/// desktop's answer is the drawer beside the grid and the phone's used to be the connect bar — one flag
|
||||
/// served both. The bar is gone: the phone's answer is now a tick and the action bar across the top, and
|
||||
/// that is a different piece of state, so a caller that only opened the pane would leave this head
|
||||
/// arriving at a list with nothing on it to press.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Both are set rather than branching on which head is running, because neither view model knows and
|
||||
/// neither should have to. The one the head does not draw is inert: the desktop draws no ticks, and the
|
||||
/// phone has no drawer.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The one caller is <c>MainWindowViewModel.ConnectToRecent</c>, and what it is doing is the deliberate
|
||||
/// act both flags exist to tell apart from browsing.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RelayCommand]
|
||||
private void AskAboutHost(HostRowViewModel? row)
|
||||
{
|
||||
if (row is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OpenHostPane(row);
|
||||
ChooseHost(row);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Puts the drawer away, whichever of the three panels is in it.
|
||||
/// </summary>
|
||||
@@ -11529,62 +11467,13 @@ internal sealed partial class VaultViewModel(
|
||||
cancellationToken).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a terminal on somewhere that is not in the keychain.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>The one connection this application makes to a machine it has never been told about.</b> Everything
|
||||
/// else starts from a keychain item, and that is still the way a host anybody uses twice should be
|
||||
/// reached — it is the only way to get a key, a group's defaults, a saved username or a password that is
|
||||
/// not typed again. This is for the other case, which is real and had no answer: a box somebody has just
|
||||
/// been given the address of.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>A typed password and nothing else.</b> Offering the keychain's keys here would be a second binding
|
||||
/// resolution beside <see cref="TryBuildAuthentication"/>, and the argument against a second one is
|
||||
/// written there at length. A key is a reason to save the host.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Nothing is written to the keychain, deliberately. What <em>is</em> written, if the user answers the
|
||||
/// question, is a host-key pin — the trust decision belongs to the endpoint rather than to the item, and
|
||||
/// a machine reached this way is exactly the one whose key nobody has seen before.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// It takes no cancellation token and allows concurrent executions, for the two reasons
|
||||
/// <see cref="ConnectAsync"/> carries.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RelayCommand(AllowConcurrentExecutions = true)]
|
||||
private Task ConnectManuallyAsync() => ConnectManuallyAsync(CancellationToken.None);
|
||||
|
||||
/// <inheritdoc cref="ConnectManuallyAsync()" />
|
||||
private async Task ConnectManuallyAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!TryParseManualTarget(ManualTarget, out var endpoint, out var refusal))
|
||||
{
|
||||
ManualStatus = refusal;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ManualPassword.Length == 0)
|
||||
{
|
||||
ManualStatus = "A password is needed. Save this machine as a host to reach it with a key.";
|
||||
return;
|
||||
}
|
||||
|
||||
ManualStatus = string.Empty;
|
||||
|
||||
await ConnectToEndpointAsync(endpoint, ManualPassword, cancellationToken).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the address typed into the hosts search box, with the password typed under it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Through <see cref="ConnectToEndpointAsync"/>, so the tab, the host-key question and the connection-log
|
||||
/// entry are the ones the manual box and the desktop palette produce, and nothing is written to the
|
||||
/// entry are the ones the connect prompt and the desktop palette produce, and nothing is written to the
|
||||
/// keychain.
|
||||
/// </para>
|
||||
/// <para>
|
||||
@@ -11654,14 +11543,27 @@ internal sealed partial class VaultViewModel(
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The second half of <see cref="ConnectManuallyAsync(CancellationToken)"/>, split out for the desktop's
|
||||
/// quick-connect palette. That head has no manual box for <see cref="ManualTarget"/> to be typed into —
|
||||
/// the palette's own query is the box — so it parses with <see cref="TryParseManualTarget"/> and hands
|
||||
/// over what it read rather than writing into the phone's fields to have them read back here.
|
||||
/// <b>The one connection this application makes to a machine it has never been told about.</b> Everything
|
||||
/// else starts from a keychain item, and that is still the way a host anybody uses twice should be
|
||||
/// reached — it is the only way to get a key, a group's defaults, a saved username or a password that is
|
||||
/// not typed again. This is for the other case, which is real and had no answer: a box somebody has just
|
||||
/// been given the address of.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The caller has already refused an unreadable address and an empty password, which is why neither is
|
||||
/// checked again: both refusals are sentences about a box, and only the caller knows which box.
|
||||
/// <b>A typed password and nothing else.</b> Offering the keychain's keys here would be a second binding
|
||||
/// resolution beside <see cref="TryBuildAuthentication"/>, and the argument against a second one is
|
||||
/// written there at length. A key is a reason to save the host.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Nothing is written to the keychain, deliberately. What <em>is</em> written, if the user answers the
|
||||
/// question, is a host-key pin — the trust decision belongs to the endpoint rather than to the item, and
|
||||
/// a machine reached this way is exactly the one whose key nobody has seen before.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Every box an address is typed into — the connect prompt, the desktop's palette, the phone's hosts
|
||||
/// search — parses it with <see cref="TryParseManualTarget"/> and hands over what it read. The caller has
|
||||
/// already refused an unreadable address and an empty password, which is why neither is checked again:
|
||||
/// both refusals are sentences about a box, and only the caller knows which box.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal Task ConnectToEndpointAsync(
|
||||
@@ -12375,7 +12277,7 @@ internal sealed partial class VaultViewModel(
|
||||
// Last, and after the tab exists: keeping the password is a favour, and the session the user asked
|
||||
// for must not wait on a vault write to appear.
|
||||
//
|
||||
// Only for a target that came from a keychain host. A machine typed into the manual box has nothing
|
||||
// Only for a target that came from a keychain host. A machine typed in as an address has nothing
|
||||
// to bind a credential to and nothing to bind it *on* — that path saves nothing by design, and the
|
||||
// screen it is typed on says so.
|
||||
if (target is { Row: { } row, MayRememberPassword: true })
|
||||
@@ -12702,7 +12604,7 @@ internal sealed partial class VaultViewModel(
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only a keychain host can be: the relay dials the address the server holds for an item, Spelunk is
|
||||
/// reached with a key the item names, and a machine typed into the manual box has neither.
|
||||
/// reached with a key the item names, and a machine typed in as an address has neither.
|
||||
/// </remarks>
|
||||
internal bool Routed => Row?.Host is { } host && (host.RelayEnabled || host.Spelunk is not null);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user