Public Access
Give the hosts board its vaults, and the Hetzner import a vault and a key
Six things, all of them answers to a question a screen was deciding on somebody's behalf. The board draws a heading per vault and sorts each vault's hosts by that vault's own groups. A group is an item of one vault, so drawing every group of every vault in one run flattened away containment that already exists: two vaults each holding a "production" were two headings with the same word on them, told apart by a small grey name beside each. Single- vault sessions are unchanged. There is now one "No group" heading per vault, so the folded set is keyed on SidebarGroupHeader.CollapseKey rather than on GroupId ?? Guid.Empty, which had one vault's unfiled hosts folding every other vault's away with them. A Ctrl-click starts its set with the card the board has lit. The board always has a selection — a fresh unlock lights the first host so CONNECT has something under it — so "one card lit, nothing ticked" is the state this screen opens in, and ticking a second card there produced a set of one: the machine the user was looking at was left out of the count and out of everything Delete, Move and Change group then ran over. The card tooltip says the host's name as well as its address. 214 pixels of card trims the name, and the tooltip was the one place it was not said in full. The card's floor drops from 120 to 104. A card with no tags measures about 87, so the mock's number was thirty pixels of air under every card on a board that is scrolled. "Move to another vault…" joins the board's context menu for one host. Moving one machine meant opening its pane and finding the menu inside it, while moving six was a right click away — the one-host case was the harder of the two. And the Hetzner import asks which vault and which key. A project belongs to a team more often than to a person, so forty servers landing wherever the session happens to be standing is the wrong default; ImportHostsAsync takes the target vault, and the ssh_config importer passes null and is unchanged. The key is the half Hetzner cannot supply and the keychain usually already holds — the public half went to Hetzner when the servers were made — so the import points every host at it rather than bringing one. It defaults to no key rather than guessing, and says so when the key it was given lives in a different vault from the hosts.
This commit is contained in:
1 parent
ed936380b3
commit
c859705b83
9 files changed
+926
-84
No files matched your search
@@ -405,6 +405,84 @@ public sealed partial class ShellFlowTests
|
||||
// ---- Helpers ----
|
||||
|
||||
/// <summary>A server carrying enough to be imported, used wherever the shape is not what is being tested.</summary>
|
||||
/// <summary>
|
||||
/// The two answers this import used to decide on somebody's behalf: which vault, and which key.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// ◆ <b>A Hetzner project is the case where "wherever I happen to be standing" is the wrong vault.</b>
|
||||
/// The <c>ssh_config</c> importer reads this machine's own account and files into the active vault
|
||||
/// fairly; a project belongs to a team more often than to a person, and forty servers landing in
|
||||
/// somebody's personal vault are either forty items nobody else can see or forty moved one at a time.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The key is the half Hetzner cannot supply and the keychain usually already holds.</b> The public
|
||||
/// half went to Hetzner when the servers were made; the private half never did. So the import points at
|
||||
/// a key rather than bringing one — the host stores its id, exactly as one bound by hand does, and the
|
||||
/// material is neither read nor copied. Without it this screen's honest answer was forty hosts asking
|
||||
/// for a password.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The key here is deliberately left in the personal vault while the hosts go to the shared one, which
|
||||
/// is allowed and is the one thing about it worth saying out loud: a binding resolves across every vault
|
||||
/// <em>this</em> account can read, so these hosts connect here and not for a colleague who holds only
|
||||
/// the destination's key.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task ImportingIntoAChosenVault_BindsEveryServerToTheChosenKey()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
|
||||
var vaults = shell.Vaults;
|
||||
|
||||
await vaults.LoadAsync(Token);
|
||||
|
||||
vaults.NewVaultCommand.Execute(null);
|
||||
vaults.NewVaultName = "Platform secrets";
|
||||
|
||||
await vaults.CreateVaultCommand.ExecuteAsync(null);
|
||||
|
||||
var vault = shell.Vault!;
|
||||
var sharedVaultId = vaults.SelectedVault!.VaultId;
|
||||
|
||||
await vault.LoadAsync(Token);
|
||||
await AddKeyAsync(vault, "deploy");
|
||||
|
||||
var keyId = vault.Keys.ShouldHaveSingleItem().EntityId;
|
||||
|
||||
var wizard = Wizard(Project(WebServer));
|
||||
|
||||
wizard.Token = HetznerToken;
|
||||
|
||||
await wizard.ScanCommand.ExecuteAsync(null);
|
||||
|
||||
wizard.HasVaultChoice.ShouldBeTrue("there are two vaults to choose between");
|
||||
wizard.HasKeyChoice.ShouldBeTrue("the keychain has a key to point at");
|
||||
wizard.SelectedKey.ShouldNotBeNull().KeyId.ShouldBeNull("binding a key is chosen, never defaulted");
|
||||
|
||||
wizard.SelectedVault = wizard.VaultChoices.Single(choice => choice.VaultId == sharedVaultId);
|
||||
wizard.SelectedKey = wizard.KeyChoices.Single(choice => choice.KeyId == keyId);
|
||||
|
||||
wizard.AuthenticationNote.ShouldContain("deploy");
|
||||
wizard.AuthenticationNote.ShouldContain(
|
||||
"another vault",
|
||||
Case.Sensitive,
|
||||
"the key stayed behind, and the colleagues these hosts just joined cannot resolve it");
|
||||
|
||||
await wizard.ImportCommand.ExecuteAsync(null);
|
||||
|
||||
var imported = vault.Hosts.Single(row => string.Equals(row.Label, "web-01", StringComparison.Ordinal));
|
||||
|
||||
imported.VaultId.ShouldBe(sharedVaultId, wizard.Status);
|
||||
imported.Host.SshKeyId.ShouldBe(keyId, "pointed at the key rather than carrying one");
|
||||
wizard.Status.ShouldContain("Platform secrets");
|
||||
|
||||
// Through the ordinary path, as every other write here is: it reached the server without anything
|
||||
// pressing Sync.
|
||||
server.LiveRowCount.ShouldBe(2, "the key and the host");
|
||||
}
|
||||
|
||||
private const string WebServer = """
|
||||
{
|
||||
"id": 2,
|
||||
|
||||
Reference in new issue
Block a user