mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 22:55:24 +00:00
feat: host lock api so a plugin can load hosts from a config file (#1581)
This commit is contained in:
1 parent
1cdbfc674c
commit
2c785fe35b
34 files changed
+1388
-52
No files matched your search
@@ -698,6 +698,36 @@ export interface PluginHostCreateInput {
|
||||
sshOptions?: HostSshOptions | null;
|
||||
/** Host settings by plugin id, the shape a Termix export carries. */
|
||||
pluginSettings?: Record<string, Record<string, unknown>>;
|
||||
/**
|
||||
* Each plugin's own host switch (its manifest enableKey), by plugin id.
|
||||
* A plugin that is not running is skipped and logged; one with no switch
|
||||
* is refused.
|
||||
*/
|
||||
pluginEnable?: Record<string, boolean>;
|
||||
/**
|
||||
* Secrets, stored encrypted with the owner's data key and never returned
|
||||
* in a PluginHostRecord. An empty string or null clears the stored value.
|
||||
*/
|
||||
password?: string | null;
|
||||
key?: string | null;
|
||||
keyPassword?: string | null;
|
||||
keyType?: string | null;
|
||||
sudoPassword?: string | null;
|
||||
/**
|
||||
* Logins for plugin protocols (contributes.protocols), keyed by protocol
|
||||
* id. null removes that protocol's login. Unknown protocol ids are skipped.
|
||||
*/
|
||||
protocolAuth?: Record<string, PluginHostProtocolAuthInput | null>;
|
||||
}
|
||||
|
||||
/** One protocol login on a host write. Absent keys keep what is stored. */
|
||||
export interface PluginHostProtocolAuthInput {
|
||||
authType?: "direct" | "credential" | "none";
|
||||
credentialId?: number | null;
|
||||
username?: string | null;
|
||||
password?: string | null;
|
||||
/** The protocol's declared credentialFields, secret ones included. */
|
||||
fields?: Record<string, string>;
|
||||
}
|
||||
|
||||
/** Fields a plugin may change on a host the acting user owns. */
|
||||
@@ -725,6 +755,13 @@ export const PLUGIN_HOST_INPUT_KEYS = [
|
||||
"forceKeyboardInteractive",
|
||||
"sshOptions",
|
||||
"pluginSettings",
|
||||
"pluginEnable",
|
||||
"password",
|
||||
"key",
|
||||
"keyPassword",
|
||||
"keyType",
|
||||
"sudoPassword",
|
||||
"protocolAuth",
|
||||
] as const satisfies readonly (keyof PluginHostCreateInput)[];
|
||||
|
||||
/** The same shape canAccessHost returns internally, without secrets. */
|
||||
@@ -761,6 +798,34 @@ export interface PluginShareableRole {
|
||||
displayName: string | null;
|
||||
}
|
||||
|
||||
/** One grant in ctx.hosts.setShares. */
|
||||
export interface PluginHostShareGrant {
|
||||
target: PluginShareTarget;
|
||||
permissionLevel: PluginHostShareLevel;
|
||||
/** Omit for a grant that never expires. */
|
||||
durationHours?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hosts a plugin manages itself, for example from a config file. Core refuses
|
||||
* edits, deletes and share changes on a locked host from the UI, the API and
|
||||
* other plugins, and marks it in host reads so a layout can show it read only.
|
||||
* Pins and order stay with the user. Only the plugin that locks a host may
|
||||
* still write it.
|
||||
*/
|
||||
export interface PluginHostLockProvider {
|
||||
isLocked: (hostId: number) => boolean;
|
||||
/** A short line shown under the managed note, or null. */
|
||||
note: (hostId: number) => string | null;
|
||||
/** False blocks adding hosts by hand (editor, import, other plugins). */
|
||||
allowManualCreate: () => boolean;
|
||||
}
|
||||
|
||||
export interface PluginHostLocksApi {
|
||||
/** Registers this plugin's locks. Needs hosts:lock. Removed on deactivate. */
|
||||
provide: (provider: PluginHostLockProvider) => () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hosts the acting user can see, and the RBAC + sharing operations a plugin
|
||||
* needs to build a feature (like fleets) on top of hosts it does not own.
|
||||
@@ -823,6 +888,15 @@ export interface PluginHosts {
|
||||
permissionLevel: PluginHostShareLevel,
|
||||
durationHours?: number,
|
||||
) => Promise<PluginHostShareResult>;
|
||||
/**
|
||||
* Replaces every grant on a host the acting user owns with this list.
|
||||
* Grants that stay keep their row and counters, missing ones are revoked.
|
||||
* Needs hosts:write.
|
||||
*/
|
||||
setShares: (
|
||||
hostId: number,
|
||||
grants: PluginHostShareGrant[],
|
||||
) => Promise<PluginHostShareResult>;
|
||||
/** Users the caller may pick as a share target, for building a picker UI. */
|
||||
listUsers: () => Promise<PluginShareableUser[]>;
|
||||
/** Non-system roles the caller may pick as a share target. */
|
||||
@@ -896,6 +970,8 @@ export interface PluginHosts {
|
||||
) => () => void;
|
||||
/** The recent activity list on the dashboard. */
|
||||
activity: PluginHostActivityApi;
|
||||
/** Read only hosts this plugin manages. */
|
||||
locks: PluginHostLocksApi;
|
||||
}
|
||||
|
||||
export interface PluginHostImportOptions {
|
||||
@@ -2011,6 +2087,15 @@ export interface PluginSshKeyCredential {
|
||||
publicKey: string | null;
|
||||
}
|
||||
|
||||
/** A saved credential without its secrets. */
|
||||
export interface PluginCredentialSummary {
|
||||
id: number;
|
||||
name: string;
|
||||
username: string | null;
|
||||
authType: string;
|
||||
folder: string | null;
|
||||
}
|
||||
|
||||
/** A key pair to save as a new credential for the acting user. */
|
||||
export interface PluginSshKeyCredentialInput {
|
||||
name: string;
|
||||
@@ -2032,6 +2117,12 @@ export interface PluginCredentials {
|
||||
* Needs credentials:use and the core credentials.view permission. Audited.
|
||||
*/
|
||||
listSshKeys: () => Promise<PluginSshKeyCredential[]>;
|
||||
/**
|
||||
* The acting user's own credentials by name, with no secret material, so a
|
||||
* plugin can point a host at one. Needs credentials:use and the core
|
||||
* credentials.view permission. Audited.
|
||||
*/
|
||||
list: () => Promise<PluginCredentialSummary[]>;
|
||||
/**
|
||||
* Saves a key pair as a new encrypted credential owned by the acting user
|
||||
* and returns its id. Needs credentials:write, the core credentials.create
|
||||
|
||||
@@ -61,6 +61,7 @@ export const CAPABILITY_CATALOG: readonly CapabilityInfo[] = [
|
||||
entry("network:probe", "medium"),
|
||||
entry("hosts:status-provide", "medium"),
|
||||
entry("hosts:activity-provide", "medium"),
|
||||
entry("hosts:lock", "medium"),
|
||||
entry("users:read", "medium"),
|
||||
entry("events:core", "medium"),
|
||||
entry("notify:send", "medium"),
|
||||
|
||||
@@ -37,7 +37,7 @@ export {
|
||||
* "^1.2" needs 1.2 or later). engine.termix is checked against the core
|
||||
* version by the server.
|
||||
*/
|
||||
export const PLUGIN_API_VERSION = "1.5.0";
|
||||
export const PLUGIN_API_VERSION = "1.6.0";
|
||||
|
||||
/** The API major, the default engine.api for a new plugin. */
|
||||
export const SUPPORTED_PLUGIN_API_VERSION = String(
|
||||
|
||||
@@ -76,6 +76,9 @@ import type {
|
||||
PluginHostProbeTarget,
|
||||
PluginHostStatusEntry,
|
||||
PluginActivityProvider,
|
||||
PluginHostLockProvider,
|
||||
PluginCredentialSummary,
|
||||
PluginHostShareGrant,
|
||||
PluginHostStatusProvider,
|
||||
PluginNotification,
|
||||
PluginNotificationChannel,
|
||||
@@ -140,6 +143,8 @@ export interface FakeContextOptions {
|
||||
protocolTargets?: Record<string, PluginProtocolTarget>;
|
||||
/** What ctx.credentials.listSshKeys answers. */
|
||||
sshKeyCredentials?: PluginSshKeyCredential[];
|
||||
/** What ctx.credentials.list answers. */
|
||||
credentialSummaries?: PluginCredentialSummary[];
|
||||
/** What ctx.desktop.available() answers. Defaults to false. */
|
||||
desktopAvailable?: boolean;
|
||||
/** What ctx.desktop.local is. Calls throw when this is not given. */
|
||||
@@ -285,6 +290,8 @@ export interface FakePluginContext {
|
||||
hostStatusProviders: PluginHostStatusProvider[];
|
||||
/** Every provider passed to ctx.hosts.activity.provide that is still registered. */
|
||||
activityProviders: PluginActivityProvider[];
|
||||
/** Every provider passed to ctx.hosts.locks.provide that is still registered. */
|
||||
hostLockProviders: PluginHostLockProvider[];
|
||||
/** Topics emitted through ctx.events.emit, in order. */
|
||||
emitted: Array<{ topic: string; payload: unknown }>;
|
||||
/** Backing store behind ctx.kv. */
|
||||
@@ -317,6 +324,8 @@ export interface FakePluginContext {
|
||||
permissionLevel: string;
|
||||
durationHours?: number;
|
||||
}>;
|
||||
/** Every ctx.hosts.setShares call, in order. */
|
||||
hostShareSets: Array<{ hostId: number; grants: PluginHostShareGrant[] }>;
|
||||
/** Everything registered through ctx.auth. */
|
||||
auth: FakeAuthRegistrations;
|
||||
/** Every ctx.desktop.openIsolatedWindow call, in order. */
|
||||
@@ -488,7 +497,14 @@ function applyHostInput(
|
||||
const {
|
||||
tags,
|
||||
pluginSettings: _settings,
|
||||
pluginEnable: _pluginEnable,
|
||||
forceKeyboardInteractive: _fki,
|
||||
password: _password,
|
||||
key: _key,
|
||||
keyPassword: _keyPassword,
|
||||
keyType: _keyType,
|
||||
sudoPassword: _sudoPassword,
|
||||
protocolAuth: _protocolAuth,
|
||||
...rest
|
||||
} = input;
|
||||
const next: PluginHostRecord = { ...record };
|
||||
@@ -517,6 +533,7 @@ export function createFakeContext(
|
||||
const emitted: Array<{ topic: string; payload: unknown }> = [];
|
||||
const hostStatusProviders: PluginHostStatusProvider[] = [];
|
||||
const activityProviders: PluginActivityProvider[] = [];
|
||||
const hostLockProviders: PluginHostLockProvider[] = [];
|
||||
const kv = new Map<string, unknown>();
|
||||
const tables: PluginTableDefinition[] = [];
|
||||
const syncEntities: SyncEntityRegistration[] = [];
|
||||
@@ -534,6 +551,7 @@ export function createFakeContext(
|
||||
}>();
|
||||
const sshConnections: FakePluginContext["sshConnections"] = [];
|
||||
const hostShares: FakePluginContext["hostShares"] = [];
|
||||
const hostShareSets: FakePluginContext["hostShareSets"] = [];
|
||||
const desktopWindows: FakePluginContext["desktopWindows"] = [];
|
||||
const externalClientLaunches: FakePluginContext["externalClientLaunches"] =
|
||||
[];
|
||||
@@ -1004,6 +1022,10 @@ export function createFakeContext(
|
||||
hostShares.push({ hostId, targets, permissionLevel, durationHours });
|
||||
return { hostId, shared: true };
|
||||
},
|
||||
setShares: async (hostId, grants) => {
|
||||
hostShareSets.push({ hostId, grants: [...grants] });
|
||||
return { hostId, shared: true };
|
||||
},
|
||||
listUsers: async () => options.shareableUsers ?? [],
|
||||
listRoles: async () => options.shareableRoles ?? [],
|
||||
trackSession: (hostId) => {
|
||||
@@ -1034,6 +1056,15 @@ export function createFakeContext(
|
||||
};
|
||||
},
|
||||
},
|
||||
locks: {
|
||||
provide: (provider) => {
|
||||
hostLockProviders.push(provider);
|
||||
return () => {
|
||||
const at = hostLockProviders.indexOf(provider);
|
||||
if (at >= 0) hostLockProviders.splice(at, 1);
|
||||
};
|
||||
},
|
||||
},
|
||||
status: {
|
||||
provide: (provider) => {
|
||||
hostStatusProviders.push(provider);
|
||||
@@ -1286,6 +1317,7 @@ export function createFakeContext(
|
||||
},
|
||||
|
||||
credentials: {
|
||||
list: async () => [...(options.credentialSummaries ?? [])],
|
||||
listSshKeys: async () => [
|
||||
...(options.sshKeyCredentials ?? []),
|
||||
...createdSshKeys.map((key) => ({
|
||||
@@ -1468,6 +1500,7 @@ export function createFakeContext(
|
||||
disposals,
|
||||
hostStatusProviders,
|
||||
activityProviders,
|
||||
hostLockProviders,
|
||||
emitted,
|
||||
kv,
|
||||
tables,
|
||||
@@ -1478,6 +1511,7 @@ export function createFakeContext(
|
||||
coreSettings,
|
||||
sshConnections,
|
||||
hostShares,
|
||||
hostShareSets,
|
||||
auth,
|
||||
desktopWindows,
|
||||
externalClientLaunches,
|
||||
@@ -1560,6 +1594,8 @@ export interface MockContextOptions {
|
||||
protocolTargets?: Record<string, PluginProtocolTarget>;
|
||||
/** What ctx.credentials.listSshKeys serves. */
|
||||
sshKeyCredentials?: PluginSshKeyCredential[];
|
||||
/** What ctx.credentials.list serves. */
|
||||
credentialSummaries?: PluginCredentialSummary[];
|
||||
/** What ctx.desktop.available() answers. */
|
||||
desktopAvailable?: boolean;
|
||||
/** What ctx.desktop.local is. See FakeContextOptions. */
|
||||
@@ -1699,6 +1735,7 @@ export function createMockCtx(
|
||||
services: options.services,
|
||||
protocolTargets: options.protocolTargets,
|
||||
sshKeyCredentials: options.sshKeyCredentials,
|
||||
credentialSummaries: options.credentialSummaries,
|
||||
desktopAvailable: options.desktopAvailable,
|
||||
desktopLocal: options.desktopLocal,
|
||||
auditEntries: options.auditEntries,
|
||||
@@ -1900,6 +1937,10 @@ export function createMockCtx(
|
||||
require("hosts:write");
|
||||
return ctx.hosts.share(hostId, targets, permissionLevel, durationHours);
|
||||
},
|
||||
setShares: async (hostId, grants) => {
|
||||
require("hosts:write");
|
||||
return ctx.hosts.setShares(hostId, grants);
|
||||
},
|
||||
listUsers: async () => {
|
||||
require("hosts:write");
|
||||
return ctx.hosts.listUsers();
|
||||
@@ -1930,6 +1971,12 @@ export function createMockCtx(
|
||||
return ctx.hosts.activity.provide(provider);
|
||||
},
|
||||
},
|
||||
locks: {
|
||||
provide: (provider) => {
|
||||
require("hosts:lock");
|
||||
return ctx.hosts.locks.provide(provider);
|
||||
},
|
||||
},
|
||||
status: {
|
||||
provide: (provider) => {
|
||||
require("hosts:read");
|
||||
@@ -2098,6 +2145,10 @@ export function createMockCtx(
|
||||
},
|
||||
|
||||
credentials: {
|
||||
list: async () => {
|
||||
require("credentials:use");
|
||||
return ctx.credentials.list();
|
||||
},
|
||||
listSshKeys: async () => {
|
||||
require("credentials:use");
|
||||
return ctx.credentials.listSshKeys();
|
||||
|
||||
Reference in new issue
Block a user