Public Access
Spelunk: sign each device in through the browser instead of keeping Spelunk API keys in the vault
A Spelunk API key reaches one organisation, so a server elsewhere - oc-linux in Orange Climate - was refused before it reached the agent. Each device now signs in to a Spelunk once, under Settings -> Security -> Spelunk or from a Spelunk host's editor: the OAuth authorization code flow with PKCE for a native app (RFC 8252), with Spelunk as the authorization server. The browser opens Spelunk's /api/app-sign-in/authorize with a loopback redirect (the app's own scheme on Android), the person signs in there however they normally do and allows "DodoSSH on <device>", and the code is redeemed at /api/app-sign-in/token for an app session token. That token opens terminals in every organisation its person is in, with the role they have on each server at the time; tunnel tickets send no organisation. The sign-in stays on the device: sealed in the local cache under the LocalCacheKey (AAD resource type 18, bound to its row), never synced, wiped with the cache, and only ever sent to the Spelunk it came from. A 401 from Spelunk forgets it; SIGN OUT ends it there and here, and so does signing out of DodoSSH. Before every Spelunk connection DodoSSH asks its own identity provider whether its sign-in still stands (a forced refresh, at most once a minute), and an account it refuses is signed out of DodoSSH and of every Spelunk on the device. A route names no key any more: Spelunk hosts are written at host schema 11, which an older build shows read-only and asks to be updated for, and a payload that still names a key credential decodes with the id skipped. The SpelunkKey item type is gone: SyncEntityType 14, ChangeEntityType 14 and AAD type 17 are retired, the server drops spelunk_key and its change-log rows, and the client cache drops its cached items, outbox entries and conflicts of that type. A link still never starts a sign-in: from a Spelunk this device is not signed in to it opens the editor, which offers to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
902761d37b
commit
ef365f462e
62 files changed
+4327
-1945
No files matched your search
@@ -73,7 +73,6 @@ internal static class ItemKinds
|
||||
(IItemKind)new HostKind(), new SshKeyKind(), new CredentialKind(), new KnownHostKeyKind(),
|
||||
new HostGroupKind(), new TagKind(), new SnippetKind(),
|
||||
new ConnectionLogEntryKind(), new ActivityLogEntryKind(), new ObjectStoreKind(),
|
||||
new SpelunkKeyKind(),
|
||||
}.ToDictionary(kind => kind.WireType);
|
||||
|
||||
/// <summary>The kind for a wire type, or null when this server does not synchronise it yet.</summary>
|
||||
@@ -1094,98 +1093,3 @@ internal sealed class ObjectStoreKind : IItemKind
|
||||
/// <inheritdoc />
|
||||
public SyncPlaintextFields? Hydrate(IVaultItem item) => null;
|
||||
}
|
||||
|
||||
/// <summary>Spelunk keys: an envelope and nothing else.</summary>
|
||||
/// <remarks>
|
||||
/// <see cref="ObjectStoreKind"/>'s rules, because it holds the same class of thing: an API key is a password,
|
||||
/// and the address beside it is where somebody manages their servers. Nothing on the server calls Spelunk.
|
||||
/// </remarks>
|
||||
internal sealed class SpelunkKeyKind : IItemKind
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public SyncEntityType WireType => SyncEntityType.SpelunkKey;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ChangeEntityType ChangeType => ChangeEntityType.SpelunkKey;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IVaultItem?> FindAsync(
|
||||
DodoDbContext database,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken) =>
|
||||
await database.SpelunkKeys.SingleOrDefaultAsync(s => s.Id == id, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Dictionary<Guid, IVaultItem>> LoadAsync(
|
||||
DodoDbContext database,
|
||||
Guid vaultId,
|
||||
Guid[] ids,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var rows = await database.SpelunkKeys
|
||||
.Where(s => s.VaultId == vaultId && ids.Contains(s.Id))
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return rows.ToDictionary(row => row.Id, row => (IVaultItem)row);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IVaultItem Add(DodoDbContext database, Guid id, Guid vaultId)
|
||||
{
|
||||
var key = new VaultSpelunkKey { Id = id, VaultId = vaultId };
|
||||
|
||||
database.SpelunkKeys.Add(key);
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
/// <summary>Refuses every plaintext field there is.</summary>
|
||||
/// <remarks>
|
||||
/// The relay fields are refused although this type holds an address, as they are for a bucket: the
|
||||
/// address belongs in the ciphertext, and the server has no business keeping a list of where its users
|
||||
/// manage their servers from.
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
public bool ValidateFields(SyncPlaintextFields fields, out string error)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(fields);
|
||||
|
||||
error = string.Empty;
|
||||
|
||||
if (fields.RelayEnabled || fields.Hostname is not null || fields.Port is not null)
|
||||
{
|
||||
error = "A Spelunk key is not something the server dials; its address stays encrypted.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fields.GroupId is not null || fields.ParentId is not null || fields.RelatedId is not null)
|
||||
{
|
||||
error = "A Spelunk key's contents are inside its payload.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fields.Kind is not null || fields.PublicKeyFingerprint is not null)
|
||||
{
|
||||
error = "A Spelunk key carries no plaintext fields at all.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <remarks>Nothing to copy: this type has no plaintext columns to copy anything into.</remarks>
|
||||
/// <inheritdoc />
|
||||
public void ApplyFields(IVaultItem item, SyncPlaintextFields fields)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearFieldsOnDelete(IVaultItem item)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public SyncPlaintextFields? Hydrate(IVaultItem item) => null;
|
||||
}
|
||||
@@ -56,7 +56,6 @@ internal sealed class TombstoneCollector(
|
||||
collected += await CollectAsync(db => db.ConnectionLog, ChangeEntityType.ConnectionLogEntry, before, cancellationToken).ConfigureAwait(false);
|
||||
collected += await CollectAsync(db => db.ActivityLog, ChangeEntityType.ActivityLogEntry, before, cancellationToken).ConfigureAwait(false);
|
||||
collected += await CollectAsync(db => db.ObjectStores, ChangeEntityType.ObjectStore, before, cancellationToken).ConfigureAwait(false);
|
||||
collected += await CollectAsync(db => db.SpelunkKeys, ChangeEntityType.SpelunkKey, before, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
collected += await CompactAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
||||
Reference in new issue
Block a user