mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat: ssh transport providers, core ssh relay and plugin folders api
This commit is contained in:
1 parent
3caf6cca62
commit
901a235de4
20 files changed
+736
-77
No files matched your search
@@ -7,6 +7,8 @@ Plugin API 1.8.0.
|
||||
### Added
|
||||
|
||||
- `ctx.sync.provide` and the `sync:provide` capability, so a plugin can answer every /sync request
|
||||
- `ctx.ssh.transports.provide` and the `ssh:transport` capability, so a plugin can carry SSH connections over its own stream
|
||||
- `ctx.hosts.folders.ensure` and `removeEmpty`
|
||||
|
||||
### Removed
|
||||
|
||||
|
||||
@@ -202,38 +202,40 @@ Bundled as ESM for Node 22+ into `dist/backend.js`. Backend imports of your own
|
||||
|
||||
### What ctx has
|
||||
|
||||
| Member | Capability | What it does |
|
||||
| ---------------------- | -------------------------------- | -------------------------------------------------------------- |
|
||||
| `pluginId`, `manifest` | none | Your id and manifest |
|
||||
| `log` | none | Log lines tagged with your plugin |
|
||||
| `db` | `db:own` | Your own tables |
|
||||
| `kv` | `kv:own` | Small key value state |
|
||||
| `files.dataDir()` | `files:own` | A folder of your own |
|
||||
| `settings` | none | Your settings |
|
||||
| `secrets` | `secrets:own` | Encrypted values per user, `seal`/`unseal` for your own tables |
|
||||
| `http`, `ws` | `network:serve` | Routes and sockets |
|
||||
| `rbac` | none | Check the actor's permissions |
|
||||
| `hosts` | `hosts:read`, `hosts:write` | List, read, create, update and share hosts as the actor |
|
||||
| `ssh` | `ssh:connect`, `credentials:use` | Connect to hosts through Termix's SSH pipeline |
|
||||
| `credentials` | varies | The actor's saved keys, and protocol logins |
|
||||
| `services` | per service | Call other plugins and offer your own |
|
||||
| `events` | `events:core` for core topics | Emit and listen for events |
|
||||
| `notify` | `notify:send` | Send alerts to the Alerts inbox |
|
||||
| `fetch` | `network:outbound` | Outbound HTTP with SSRF protection |
|
||||
| `process` | `process:spawn` | Run programs on the server |
|
||||
| `schedule` | none | Timers that stop on disable and never overlap |
|
||||
| `audit.record` | none | Write your own audit line |
|
||||
| `sync` | none | Sync your tables to a linked desktop |
|
||||
| `auth` | `auth:provide` | Login methods, second factors, SSH auth types |
|
||||
| `auth.issueUserToken` | `users:impersonate` | A short-lived session for calling Termix's API as a user |
|
||||
| `auth.policy` | `auth:policy` | Login rules: idle timeout, session length, second factors |
|
||||
| `sync.provide` | `sync:provide` | Answers all /sync requests: server sync, linked desktops |
|
||||
| `users.getLocale` | `users:read` | A user's UI language |
|
||||
| `system` | `system:tls` | The server's HTTPS certificate |
|
||||
| `plugins.list()` | `plugins:read` | Which plugins are installed |
|
||||
| `asUser(userId, fn)` | always audited | Run background work as a user |
|
||||
| `currentActor()` | none | The user the current call runs for |
|
||||
| `disposables.add(fn)` | none | Extra cleanup on disable |
|
||||
| Member | Capability | What it does |
|
||||
| ---------------------- | -------------------------------- | --------------------------------------------------------------- |
|
||||
| `pluginId`, `manifest` | none | Your id and manifest |
|
||||
| `log` | none | Log lines tagged with your plugin |
|
||||
| `db` | `db:own` | Your own tables |
|
||||
| `kv` | `kv:own` | Small key value state |
|
||||
| `files.dataDir()` | `files:own` | A folder of your own |
|
||||
| `settings` | none | Your settings |
|
||||
| `secrets` | `secrets:own` | Encrypted values per user, `seal`/`unseal` for your own tables |
|
||||
| `http`, `ws` | `network:serve` | Routes and sockets |
|
||||
| `rbac` | none | Check the actor's permissions |
|
||||
| `hosts` | `hosts:read`, `hosts:write` | List, read, create, update and share hosts as the actor |
|
||||
| `ssh` | `ssh:connect`, `credentials:use` | Connect to hosts through Termix's SSH pipeline |
|
||||
| `credentials` | varies | The actor's saved keys, and protocol logins |
|
||||
| `services` | per service | Call other plugins and offer your own |
|
||||
| `events` | `events:core` for core topics | Emit and listen for events |
|
||||
| `notify` | `notify:send` | Send alerts to the Alerts inbox |
|
||||
| `fetch` | `network:outbound` | Outbound HTTP with SSRF protection |
|
||||
| `process` | `process:spawn` | Run programs on the server |
|
||||
| `schedule` | none | Timers that stop on disable and never overlap |
|
||||
| `audit.record` | none | Write your own audit line |
|
||||
| `sync` | none | Sync your tables to a linked desktop |
|
||||
| `auth` | `auth:provide` | Login methods, second factors, SSH auth types |
|
||||
| `auth.issueUserToken` | `users:impersonate` | A short-lived session for calling Termix's API as a user |
|
||||
| `auth.policy` | `auth:policy` | Login rules: idle timeout, session length, second factors |
|
||||
| `sync.provide` | `sync:provide` | Answers all /sync requests: server sync, linked desktops |
|
||||
| `ssh.transports` | `ssh:transport` | Carries a host's SSH connection over your own stream |
|
||||
| `hosts.folders` | `hosts:write` | Create a folder row with a color and icon, remove it when empty |
|
||||
| `users.getLocale` | `users:read` | A user's UI language |
|
||||
| `system` | `system:tls` | The server's HTTPS certificate |
|
||||
| `plugins.list()` | `plugins:read` | Which plugins are installed |
|
||||
| `asUser(userId, fn)` | always audited | Run background work as a user |
|
||||
| `currentActor()` | none | The user the current call runs for |
|
||||
| `disposables.add(fn)` | none | Extra cleanup on disable |
|
||||
|
||||
### The actor
|
||||
|
||||
@@ -302,6 +304,9 @@ router.post("/notes", async (req, res) => {
|
||||
- `ctx.rbac.require("manage")` on a single route, `await ctx.rbac.has("manage")` in code. A full id like
|
||||
`hosts.edit` checks a core permission.
|
||||
- Host access: `ctx.hosts.checkAccess(hostId, "connect" | "view" | "edit" | "manage")`.
|
||||
- Transports: with `ssh:transport`, `ctx.ssh.transports.provide({ claims(hostId), open({ id, ip, port }) })`
|
||||
returns a stream the SSH client uses instead of a direct, proxy or jump host connection. Core also serves
|
||||
`/plugin-ws/core/ssh-relay?hostId=` for callers with connect access, so one Termix can relay to another.
|
||||
- Managed hosts: with `hosts:lock`, `ctx.hosts.locks.provide({ isLocked, note, allowManualCreate })` makes
|
||||
the hosts you manage read only for the UI, the API and other plugins (pins and order still work), and
|
||||
`allowManualCreate: () => false` blocks adding hosts by hand. Your own `ctx.hosts` writes still go
|
||||
@@ -902,47 +907,48 @@ To run your own registry instead, see https://docs.termix.site/develop/registrie
|
||||
|
||||
## Capabilities
|
||||
|
||||
| Capability | Risk | What it allows |
|
||||
| ------------------------ | -------- | ------------------------------------------------------- |
|
||||
| `credentials:read` | critical | Read stored secrets for hosts the user can reach |
|
||||
| `system:tls` | critical | Replace the HTTPS certificate, answer CA domain checks |
|
||||
| `ssh:connect` | high | Run any command on hosts, with the user's access |
|
||||
| `process:spawn` | high | Run programs on the Termix server |
|
||||
| `users:write` | high | Add users and change their roles |
|
||||
| `users:impersonate` | high | Act as another user, or get a session for them, audited |
|
||||
| `db:core-refs` | high | Query core users, roles and hosts tables (read only) |
|
||||
| `auth:provide` | high | Add login methods, second factors, SSH auth types |
|
||||
| `auth:policy` | high | Set login rules: idle timeout, session length, 2FA |
|
||||
| `sync:provide` | high | Run sync: answer every /sync request |
|
||||
| `device:serial` | high | Open serial and USB ports on the server |
|
||||
| `desktop:local` | high | Use the user's own computer, desktop app only |
|
||||
| `hosts:export` | high | Export hosts with their saved logins |
|
||||
| `notify:hub` | high | Receive every alert (the alerts hub only) |
|
||||
| `ui:layout` | high | Replace the whole app layout |
|
||||
| `hosts:write` | medium | Add and edit hosts |
|
||||
| `hosts:status-provide` | medium | Be the source of host status dots |
|
||||
| `hosts:activity-provide` | medium | Be the source of the recent activity list |
|
||||
| `hosts:lock` | medium | Make hosts it manages read only for everyone else |
|
||||
| `network:probe` | medium | Check that a host answers on a TCP port |
|
||||
| `credentials:use` | medium | Connect with saved credentials without seeing them |
|
||||
| `credentials:write` | medium | Save new credentials |
|
||||
| `network:outbound` | medium | Outbound HTTP through `ctx.fetch` |
|
||||
| `network:serve` | medium | HTTP routes and sockets |
|
||||
| `network:broadcast` | medium | Local network broadcasts, like Wake-on-LAN |
|
||||
| `users:read` | medium | Usernames, roles and UI language |
|
||||
| `events:core` | medium | Listen to core events |
|
||||
| `notify:send` | medium | Send alerts |
|
||||
| `audit:read` | medium | Read the audit log |
|
||||
| `desktop:window` | medium | Open windows in the desktop app |
|
||||
| `hosts:read` | low | Read hosts the user can see |
|
||||
| `db:own` | low | Your own tables |
|
||||
| `kv:own` | low | Small key value state |
|
||||
| `files:own` | low | Your own folder on disk |
|
||||
| `secrets:own` | low | Your own encrypted secrets |
|
||||
| `settings:read-core` | low | Read Termix server settings |
|
||||
| `plugins:read` | low | See installed plugins |
|
||||
| `ui:surface` | low | Add tabs, panels and settings |
|
||||
| `ui:theme` | low | Add color themes, CSS only |
|
||||
| Capability | Risk | What it allows |
|
||||
| ------------------------ | -------- | ------------------------------------------------------------- |
|
||||
| `credentials:read` | critical | Read stored secrets for hosts the user can reach |
|
||||
| `system:tls` | critical | Replace the HTTPS certificate, answer CA domain checks |
|
||||
| `ssh:connect` | high | Run any command on hosts, with the user's access |
|
||||
| `process:spawn` | high | Run programs on the Termix server |
|
||||
| `users:write` | high | Add users and change their roles |
|
||||
| `users:impersonate` | high | Act as another user, or get a session for them, audited |
|
||||
| `db:core-refs` | high | Query core users, roles and hosts tables (read only) |
|
||||
| `auth:provide` | high | Add login methods, second factors, SSH auth types |
|
||||
| `auth:policy` | high | Set login rules: idle timeout, session length, 2FA |
|
||||
| `sync:provide` | high | Run sync: answer every /sync request |
|
||||
| `device:serial` | high | Open serial and USB ports on the server |
|
||||
| `desktop:local` | high | Use the user's own computer, desktop app only |
|
||||
| `hosts:export` | high | Export hosts with their saved logins |
|
||||
| `notify:hub` | high | Receive every alert (the alerts hub only) |
|
||||
| `ui:layout` | high | Replace the whole app layout |
|
||||
| `hosts:write` | medium | Add and edit hosts |
|
||||
| `hosts:status-provide` | medium | Be the source of host status dots |
|
||||
| `hosts:activity-provide` | medium | Be the source of the recent activity list |
|
||||
| `ssh:transport` | high | Carry SSH connections for hosts it claims over its own stream |
|
||||
| `hosts:lock` | medium | Make hosts it manages read only for everyone else |
|
||||
| `network:probe` | medium | Check that a host answers on a TCP port |
|
||||
| `credentials:use` | medium | Connect with saved credentials without seeing them |
|
||||
| `credentials:write` | medium | Save new credentials |
|
||||
| `network:outbound` | medium | Outbound HTTP through `ctx.fetch` |
|
||||
| `network:serve` | medium | HTTP routes and sockets |
|
||||
| `network:broadcast` | medium | Local network broadcasts, like Wake-on-LAN |
|
||||
| `users:read` | medium | Usernames, roles and UI language |
|
||||
| `events:core` | medium | Listen to core events |
|
||||
| `notify:send` | medium | Send alerts |
|
||||
| `audit:read` | medium | Read the audit log |
|
||||
| `desktop:window` | medium | Open windows in the desktop app |
|
||||
| `hosts:read` | low | Read hosts the user can see |
|
||||
| `db:own` | low | Your own tables |
|
||||
| `kv:own` | low | Small key value state |
|
||||
| `files:own` | low | Your own folder on disk |
|
||||
| `secrets:own` | low | Your own encrypted secrets |
|
||||
| `settings:read-core` | low | Read Termix server settings |
|
||||
| `plugins:read` | low | See installed plugins |
|
||||
| `ui:surface` | low | Add tabs, panels and settings |
|
||||
| `ui:theme` | low | Add color themes, CSS only |
|
||||
|
||||
An update that adds a capability waits for an admin to approve it.
|
||||
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"auth:provide",
|
||||
"auth:policy",
|
||||
"sync:provide",
|
||||
"ssh:transport",
|
||||
"system:tls",
|
||||
"device:serial",
|
||||
"hosts:write",
|
||||
|
||||
@@ -1395,6 +1395,18 @@ export interface PluginHosts {
|
||||
activity: PluginHostActivityApi;
|
||||
/** Read only hosts this plugin manages. */
|
||||
locks: PluginHostLocksApi;
|
||||
/** Folder rows for the acting user. Needs hosts:write. */
|
||||
folders: PluginHostFoldersApi;
|
||||
}
|
||||
|
||||
export interface PluginHostFoldersApi {
|
||||
/** Creates the folder row if missing and sets its color and icon. */
|
||||
ensure: (
|
||||
name: string,
|
||||
style?: { color?: string | null; icon?: string | null },
|
||||
) => Promise<void>;
|
||||
/** Removes the folder row and subfolders once no host is left in them. */
|
||||
removeEmpty: (name: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface PluginHostImportOptions {
|
||||
@@ -1777,7 +1789,29 @@ export interface PluginNetwork {
|
||||
) => Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A way to reach some hosts other than a plain TCP connection. `claims` is
|
||||
* asked for every connection, so it must be quick. `open` returns a stream
|
||||
* the SSH client runs over, like a relayed socket.
|
||||
*/
|
||||
export interface PluginSshTransportProvider {
|
||||
claims: (hostId: number) => boolean;
|
||||
open: (host: {
|
||||
id: number;
|
||||
ip: string;
|
||||
port: number;
|
||||
}) => Promise<import("node:stream").Duplex>;
|
||||
}
|
||||
|
||||
export interface PluginSshTransports {
|
||||
/** Needs ssh:transport. Removed on deactivate. */
|
||||
provide: (provider: PluginSshTransportProvider) => () => void;
|
||||
}
|
||||
|
||||
export interface PluginSsh {
|
||||
/** Other ways to reach hosts, used by every connection to them. */
|
||||
transports: PluginSshTransports;
|
||||
|
||||
/**
|
||||
* Whether a TCP port answers through the host's jump host chain. Chains are
|
||||
* shared and closed when idle. The acting user must be able to connect to
|
||||
|
||||
@@ -49,6 +49,7 @@ export const CAPABILITY_CATALOG: readonly CapabilityInfo[] = [
|
||||
entry("auth:provide", "high"),
|
||||
entry("auth:policy", "high"),
|
||||
entry("sync:provide", "high"),
|
||||
entry("ssh:transport", "high"),
|
||||
entry("device:serial", "high"),
|
||||
entry("notify:hub", "high"),
|
||||
entry("ui:layout", "high"),
|
||||
|
||||
@@ -1070,6 +1070,10 @@ export function createFakeContext(
|
||||
};
|
||||
},
|
||||
},
|
||||
folders: {
|
||||
ensure: async () => {},
|
||||
removeEmpty: async () => true,
|
||||
},
|
||||
locks: {
|
||||
provide: (provider) => {
|
||||
hostLockProviders.push(provider);
|
||||
@@ -1148,6 +1152,7 @@ export function createFakeContext(
|
||||
},
|
||||
|
||||
ssh: {
|
||||
transports: { provide: () => () => {} },
|
||||
probe: async (hostId, probeOptions) =>
|
||||
options.reachable?.(hostId, probeOptions?.port) ?? false,
|
||||
connect: async (host, connectOptions) => {
|
||||
@@ -2007,6 +2012,16 @@ export function createMockCtx(
|
||||
return ctx.hosts.activity.provide(provider);
|
||||
},
|
||||
},
|
||||
folders: {
|
||||
ensure: async (name, style) => {
|
||||
require("hosts:write");
|
||||
return ctx.hosts.folders.ensure(name, style);
|
||||
},
|
||||
removeEmpty: async (name) => {
|
||||
require("hosts:write");
|
||||
return ctx.hosts.folders.removeEmpty(name);
|
||||
},
|
||||
},
|
||||
locks: {
|
||||
provide: (provider) => {
|
||||
require("hosts:lock");
|
||||
@@ -2040,6 +2055,12 @@ export function createMockCtx(
|
||||
|
||||
ssh: {
|
||||
...ctx.ssh,
|
||||
transports: {
|
||||
provide: (provider) => {
|
||||
require("ssh:transport");
|
||||
return ctx.ssh.transports.provide(provider);
|
||||
},
|
||||
},
|
||||
probe: async (hostId, probeOptions) => {
|
||||
require("ssh:connect");
|
||||
return ctx.ssh.probe(hostId, probeOptions);
|
||||
|
||||
Reference in new issue
Block a user