mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-11-07 02:55:24 +00:00
feat: let plugins store a CA-signed certificate on a credential (#1764)
A credential that carries an SSH key can already hold a CA-signed certificate, but the only way to fill it in is the credentials editor. A plugin that issues certificates has no way to keep the credential in step, so after a renewal it keeps presenting the previous certificate and the connection fails much later with a generic authentication error. ctx.credentials.setSshKeyCertificate stores one on a key credential the acting user owns, or clears it with null. It needs credentials:write and the core credentials.update permission, refuses anything that is not a key credential because core never sends a certificate for the other auth types, and is audited like the other ctx.credentials writes. Co-authored-by: Stefan Wienert <stefan@piratopedia.de> Co-authored-by: LukeGus <bugattiguy527@gmail.com>
This commit is contained in:
7 files changed
+258
No files matched your search
@@ -6,6 +6,7 @@ Plugin API 1.15.0.
|
||||
|
||||
### Added
|
||||
|
||||
- `ctx.credentials.setSshKeyCertificate` stores an OpenSSH certificate on one of the acting user's SSH key credentials, or clears it with `null`
|
||||
- `/ui` exports `ToolbarBand` (a toolbar row outside the PanelShell toolbar), `ToolbarSelect` (a small select for those rows) and `TOOLBAR_FIT` (the classes that make every control in a row 32px)
|
||||
- `PANEL.section`, the one inner layout for a SectionCard body
|
||||
- `TextAreaField` takes `fill`, so a config or log editor uses the rest of the height
|
||||
|
||||
@@ -354,6 +354,10 @@ router.post("/notes", async (req, res) => {
|
||||
`allowManualCreate: () => false` blocks adding hosts by hand. Your own `ctx.hosts` writes still go
|
||||
through. `ctx.hosts.setShares(hostId, grants)` replaces a host's whole grant list, and
|
||||
`ctx.credentials.list()` finds the acting user's credentials by name.
|
||||
- `ctx.credentials.setSshKeyCertificate(credentialId, certificate)` stores a CA-signed OpenSSH
|
||||
certificate (a `-cert.pub` line, or `null` to clear it) on one of the acting user's SSH key
|
||||
credentials. Core then offers key plus certificate on every connection through that credential, so a
|
||||
plugin that issues certificates can keep the credential current instead of asking the user to paste it.
|
||||
- Connect policy: with `hosts:connect-policy`, `ctx.hosts.connectPolicy.provide({ check(input) })` is asked
|
||||
before every SSH config is built and every remote desktop login is handed out (`input` has `userId`,
|
||||
`hostId`, `protocol`, `purpose`, `profile`, `interactive`, `origin`). Return `{ decision: "allow" }`,
|
||||
|
||||
@@ -2789,6 +2789,18 @@ export interface PluginCredentials {
|
||||
key: string,
|
||||
passphrase?: string | null,
|
||||
) => Promise<{ key: string; passphrase?: string }>;
|
||||
/**
|
||||
* Stores a CA-signed OpenSSH certificate next to an existing SSH key
|
||||
* credential, so core offers key plus certificate when it connects through
|
||||
* that credential. Only key credentials can carry one; core refuses the
|
||||
* other auth types instead of dropping the certificate silently. Pass null
|
||||
* to clear a stored one. Needs credentials:write and the core
|
||||
* credentials.update permission. Audited.
|
||||
*/
|
||||
setSshKeyCertificate: (
|
||||
credentialId: number,
|
||||
certificate: string | null,
|
||||
) => Promise<{ id: number; changed: boolean }>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -355,6 +355,11 @@ export interface FakePluginContext {
|
||||
credentialReads: Array<{ hostId: number; protocol: string }>;
|
||||
/** Every ctx.credentials.createSshKey call, with the id it answered. */
|
||||
createdSshKeys: Array<PluginSshKeyCredentialInput & { id: number }>;
|
||||
/** Every ctx.credentials.setSshKeyCertificate call, in order. */
|
||||
certificateWrites: Array<{
|
||||
credentialId: number;
|
||||
certificate: string | null;
|
||||
}>;
|
||||
/** Every ctx.notify.send call, with the actor it ran as. */
|
||||
notifications: Array<{
|
||||
actor: string | undefined;
|
||||
@@ -576,6 +581,7 @@ export function createFakeContext(
|
||||
[];
|
||||
const credentialReads: FakePluginContext["credentialReads"] = [];
|
||||
const createdSshKeys: FakePluginContext["createdSshKeys"] = [];
|
||||
const certificateWrites: FakePluginContext["certificateWrites"] = [];
|
||||
const notifications: FakePluginContext["notifications"] = [];
|
||||
const notifyHub: FakePluginContext["notifyHub"] = { current: null };
|
||||
const operationHandlers: FakePluginContext["operationHandlers"] = new Map();
|
||||
@@ -1402,6 +1408,10 @@ export function createFakeContext(
|
||||
publicKey: async (credentialId) =>
|
||||
(options.sshKeyCredentials ?? []).find((k) => k.id === credentialId)
|
||||
?.publicKey ?? null,
|
||||
setSshKeyCertificate: async (credentialId, certificate) => {
|
||||
certificateWrites.push({ credentialId, certificate });
|
||||
return { id: credentialId, changed: true };
|
||||
},
|
||||
resolveKeyRefs: async (key, passphrase) => ({
|
||||
key,
|
||||
passphrase: passphrase ?? undefined,
|
||||
@@ -1616,6 +1626,7 @@ export function createFakeContext(
|
||||
externalClientLaunches,
|
||||
credentialReads,
|
||||
createdSshKeys,
|
||||
certificateWrites,
|
||||
notifications,
|
||||
notifyHub,
|
||||
operationHandlers,
|
||||
@@ -2307,6 +2318,10 @@ export function createMockCtx(
|
||||
require("credentials:use");
|
||||
return ctx.credentials.publicKey(credentialId);
|
||||
},
|
||||
setSshKeyCertificate: async (credentialId, certificate) => {
|
||||
require("credentials:write");
|
||||
return ctx.credentials.setSshKeyCertificate(credentialId, certificate);
|
||||
},
|
||||
resolveKeyRefs: async (key, passphrase) => {
|
||||
require("credentials:read");
|
||||
return ctx.credentials.resolveKeyRefs(key, passphrase);
|
||||
|
||||
Reference in new issue
Block a user