Files
DodoSSH/tests/DodoSSH.Client.Domain.Tests/HostFactory.cs
jaap-jan 38ab9b431b Sign in to Spelunk servers through Spelunk, and connect straight away from a Spelunk link
A Spelunk route can now say that Spelunk signs the user in. Each connection makes a throwaway Ed25519 key pair in memory, sends the public half with the ticket request ({"signIn":{"publicKey":...}}), and logs in as the account Spelunk names (root when it names none) with that key, over the agent's one-connection sshd. Such a host needs no username, key, credential or typed password: the editor greys the login out and saves none, the password box and sheet stay down, and shells, file transfer and port forwards all take the key and account from the route. The flag is nested in the spelunk object at host schema version 10; hosts without it keep their version-9 bytes.

A dodossh://spelunk/connect link now connects: to the personal vault's host tagged spelunk for that server, or, when a saved host already sends an API key to that Spelunk, to a new sign-in host saved in the personal vault with that key and the spelunk tag (made if missing). Only the first link from a Spelunk opens the editor, pre-filled with sign-in ticked and the tag on, so the key credential is chosen once and never sent to an address no saved host already sends it to.
2026-09-28 22:53:51 +02:00

124 lines
5.3 KiB
C#

namespace DodoSSH.Client.Domain.Tests;
/// <summary>
/// Builds hosts, and the groups they are filed under, so each test varies only what it is about.
/// </summary>
internal static class HostFactory
{
internal static Guid Bastion { get; } = Guid.Parse("0192f0c8-1111-7c3d-8e4f-5a6b7c8d9e01");
internal static Guid Relay { get; } = Guid.Parse("0192f0c8-2222-7c3d-8e4f-5a6b7c8d9e02");
/// <summary>A vault SSH key id, for the hosts that bind one.</summary>
internal static Guid DeployKey { get; } = Guid.Parse("0192f0c8-3333-7c3d-8e4f-5a6b7c8d9e03");
/// <summary>A group id, for the hosts that are filed under one.</summary>
internal static Guid Production { get; } = Guid.Parse("0192f0c8-4444-7c3d-8e4f-5a6b7c8d9e04");
/// <summary>A tag id, for the hosts that wear one.</summary>
internal static Guid Pci { get; } = Guid.Parse("0192f0c8-8888-7c3d-8e4f-5a6b7c8d9e08");
/// <summary>A second tag id, for the tests about two people tagging one host.</summary>
internal static Guid EuWest { get; } = Guid.Parse("0192f0c8-9999-7c3d-8e4f-5a6b7c8d9e09");
/// <summary>A forward to the database, as the host would carry it.</summary>
internal static PortForwardRule Postgres { get; } = new()
{
Id = Guid.Parse("0192f0c8-aaaa-7c3d-8e4f-5a6b7c8d9e0a"),
Kind = PortForwardKind.Local,
BindPort = 5432,
TargetHost = "db.internal",
TargetPort = 5432,
};
/// <summary>A SOCKS forward, for the tests about two forwards on one host.</summary>
internal static PortForwardRule Socks { get; } = new()
{
Id = Guid.Parse("0192f0c8-bbbb-7c3d-8e4f-5a6b7c8d9e0b"),
Kind = PortForwardKind.Dynamic,
BindPort = 1080,
};
/// <summary>A credential holding a Spelunk API key, for the hosts reached through Spelunk.</summary>
internal static Guid SpelunkKey { get; } = Guid.Parse("0192f0c8-cccc-7c3d-8e4f-5a6b7c8d9e0c");
/// <summary>A server's id in Spelunk.</summary>
internal static Guid SpelunkServer { get; } = Guid.Parse("0192f0c8-dddd-7c3d-8e4f-5a6b7c8d9e0d");
/// <summary>
/// A route to <see cref="SpelunkServer"/>, with <see cref="SpelunkKey"/> — signing in through Spelunk
/// when <paramref name="signIn"/> says so.
/// </summary>
internal static SpelunkRoute ThroughSpelunk(string apiBase = "https://api.spelunk.example.com", bool signIn = false) =>
new() { ApiBase = apiBase, ServerId = SpelunkServer, ApiKeyCredentialId = SpelunkKey, SignInThroughSpelunk = signIn };
/// <remarks>
/// <paramref name="port"/> defaults to an explicit 22 rather than to null, so a test that says nothing
/// about ports gets the host shape that existed before inheritance did — which is what nearly every
/// suite here is still about. Passing <see langword="null"/> is how a test asks for the new one.
/// </remarks>
internal static HostSecret Host(
string label = "prod-db",
string hostname = "db.internal",
int? port = 22,
string? username = "deploy",
string? notes = null,
Guid[]? jumps = null,
(string Name, string Value)[]? options = null,
bool relayEnabled = false,
Guid? sshKeyId = null,
Guid? credentialId = null,
bool? asksForPassword = null,
Guid? groupId = null,
Guid[]? tags = null,
string[]? pinnedPaths = null,
PortForwardRule[]? portForwards = null,
SpelunkRoute? spelunk = null) =>
new()
{
Label = label,
Hostname = hostname,
Port = port,
Username = username,
Notes = notes,
JumpHostIds = jumps is null ? JumpChain.Empty : JumpChain.Create(jumps),
Options = options is null
? HostOptions.Empty
: HostOptions.Create(options.Select(o => new HostOption(o.Name, o.Value))),
RelayEnabled = relayEnabled,
SshKeyId = sshKeyId,
CredentialId = credentialId,
AsksForPassword = asksForPassword,
GroupId = groupId,
TagIds = tags is null ? TagSet.Empty : TagSet.Create(tags),
PinnedPaths = pinnedPaths is null ? PinnedPathList.Empty : PinnedPathList.Create(pinnedPaths),
PortForwards = portForwards is null ? PortForwardList.Empty : PortForwardList.Create(portForwards),
Spelunk = spelunk,
};
/// <summary>
/// A group, flat and defaulting nothing unless the test says otherwise.
/// </summary>
/// <remarks>
/// The defaults of this builder are the shape a group had before it could nest or default anything,
/// which is what most suites still want: a heading with a name. A test that passes nothing here is
/// asserting about the old shape on purpose.
/// </remarks>
internal static HostGroupSecret Group(
string label = "production",
Guid? parentId = null,
int? defaultPort = null,
string? defaultUsername = null,
Guid? defaultSshKeyId = null,
Guid? defaultCredentialId = null) =>
new()
{
Label = label,
ParentId = parentId,
DefaultPort = defaultPort,
DefaultUsername = defaultUsername,
DefaultSshKeyId = defaultSshKeyId,
DefaultCredentialId = defaultCredentialId,
};
}