Files
sso-manager-node/docs/plugins.md
T

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).