6.0 KiB
Plugins
The SSO Manager runs plugins as scheduled background tasks. A plugin
type is an installed module; a plugin instance is a configured, loadable
copy of a type. You can create, edit, load/unload, run, and delete instances
from the Plugins page (or the /api/plugins API), and you can run several
instances of the same type — e.g. two Proxmox endpoints, each with its own URL
and token on its own schedule.
Per-instance secrets are stored in OpenBao at
secret/plugins/<instance-id>/conf, not in sso-secrets.js. The admin UI only
ever shows them masked (********); the plugin reads them at run time. This
needs theta-suite ≥ v1.30.1 (which grants the sso-broker OpenBao policy
secret/plugins/*); re-run ./setup.sh after upgrading.
Plugin types
A plugin type is a module under nodejs/plugins/<category>/<type>.js. The
filename basename (without .js) is the type; the parent directory is the
category. The built-ins ship under plugins/discovery/:
proxmox— Proxmox VE (URL + API token)unifi— UniFi Network controller (URL + username/password)nmap— nmap OS + port scan (a target range; no credentials)
A module exports a manifest:
module.exports = {
// Identity — `type`/`category` default to the file/dir name but can be set
// explicitly. `name`/`description` show up in the UI.
type: 'proxmox',
category: 'discovery',
name: 'Proxmox VE',
description: 'Discover VMs, containers, and nodes from a PVE endpoint.',
// Drives the admin UI form, API validation, and secret masking. Fields with
// `secret: true` are stored in OpenBao; the rest live in the DB row.
configSchema: [
{ key: 'url', label: 'API URL', type: 'url', required: true },
{ key: 'tokenId', label: 'Token ID', type: 'text', required: true },
{ key: 'tokenSecret', label: 'Token Secret', type: 'password', required: true, secret: true }
],
// "Test" button: validate the config (don't do the work). Return
// { ok: true } or { ok: false, error: '...' }. Optional.
validate: async (config) => { … },
// The work. `run` is the generalized contract name; the discovery plugins
// also keep `discover` as an alias for back-compat. For `category:
// 'discovery'`, the scheduler passes the result to the discovery reconciler.
run: async (config) => { return { resources, edges }; },
discover: async (config) => { return { resources, edges }; }
};
run(config) receives the merged non-secret config + secret values as one flat
object (e.g. { url, tokenId, tokenSecret }). For a discovery plugin it
returns { resources, edges }; the reconciler upserts them into the resource
graph attributed to the instance's slug (the discovery_sources name).
Writing a custom plugin type
Drop a .js file under nodejs/plugins/discovery/ (or a new category directory)
following the manifest above. New types are picked up at boot, so restart the
SSO Manager after adding one. Runtime load/unload is per-instance only —
adding a new type still needs a restart.
The Plugins page
Under Plugins (nav, admin-only — app_sso_admin / app_sso_directory_admin
/ app_super_admin):
- New Plugin — pick a type, name it, choose a unique slug (the discovery source name + the URL the resource graph attributes results to), set a cron schedule, and fill in the config form (secret fields are password inputs). Creating it schedules it and kicks one immediate run.
- Edit — name, cron, and non-secret config.
- Edit Secrets (key icon) — password fields, prefilled masked. Leave a field blank to keep its current value.
- Test (vial icon) — runs the plugin's
validate. - Run now (play icon) — enqueues one immediate run regardless of state.
- Load / Unload — enable/disable the schedule without deleting the instance.
- Delete — removes the schedule, the OpenBao secret namespace, and the row.
API
All endpoints are mounted at /api/plugins, require an authenticated admin
(app_sso_admin / app_sso_directory_admin / app_super_admin), and return
secret values masked.
| Method + path | Purpose |
|---|---|
GET /api/plugins/types |
list installed plugin types + their configSchema |
GET /api/plugins |
list instances (with masked secrets + last-run state) |
GET /api/plugins/:id |
one instance |
POST /api/plugins |
create — body { pluginType, name, slug, cron, config } where config is a flat object of all field values; secret fields are split into OpenBao |
PUT /api/plugins/:id |
update name/cron/enabled + non-secret config |
PUT /api/plugins/:id/secrets |
update secret fields (blank = keep) |
POST /api/plugins/:id/test |
run validate → { ok } or { ok:false, error } |
POST /api/plugins/:id/load |
enable + schedule + run now |
POST /api/plugins/:id/unload |
unschedule + disable |
POST /api/plugins/:id/run |
enqueue one immediate run |
DELETE /api/plugins/:id |
unschedule + remove OpenBao secrets + delete row |
GET /api/plugins/:id/runs |
{ lastRunAt, lastStatus, lastError } |
Scheduler internals
The scheduler (BullMQ over Redis) gives each instance
a stable JobScheduler id (plugin:<instanceId>); load/unload upsert/remove
that one schedule without disturbing the others. A daily garbage_collect job
prunes discovery resources not seen in > 7 days.
Legacy migration
Before this system, plugins were configured statically in sso-secrets.js:
module.exports = {
discovery: {
plugins: {
proxmox: { enabled: true, cron: '0 * * * *', url: '…', tokenId: '…', tokenSecret: '…' }
}
}
};
On the first boot of SSO Manager ≥ v1.17.0, if the PluginInstance table is
empty and conf.discovery.plugins has entries, one instance per configured
type is seeded automatically (secret fields copied into OpenBao). After that the
table is non-empty and the static config is ignored — manage plugins from the
UI/API instead. The migration is idempotent (guarded by the empty-table check).