diff --git a/docs/plugins.md b/docs/agents.md similarity index 61% rename from docs/plugins.md rename to docs/agents.md index 4402cda..44e6768 100644 --- a/docs/plugins.md +++ b/docs/agents.md @@ -1,23 +1,23 @@ --- layout: default -title: Discovery Plugins +title: Discovery Agents nav_order: 5 --- -# Discovery Plugins +# Discovery Agents -The SSO Manager supports a robust plugin architecture for auto-discovering devices, hosts, and services across your home lab or data center. Plugins run on a scheduled cron and feed their data into a central **Reconciliation Engine** that smartly merges information based on MAC addresses and IPs. +The SSO Manager supports a robust agent architecture for auto-discovering devices, hosts, and services across your home lab or data center. Agents run on a scheduled cron and feed their data into a central **Reconciliation Engine** that smartly merges information based on MAC addresses and IPs. -## Writing a Custom Plugin +## Writing a Custom Agent -Plugins are simple JavaScript files placed in `nodejs/plugins/discovery/`. +Agents are simple JavaScript files placed in `nodejs/agents/discovery/`. -A plugin must export a single `discover` async function that returns a standardized graph of `resources` and `edges`. +A agent must export a single `discover` async function that returns a standardized graph of `resources` and `edges`. -### Plugin Skeleton +### Agent Skeleton ```javascript -// nodejs/plugins/discovery/my_custom_plugin.js +// nodejs/agents/discovery/my_custom_agent.js module.exports = { discover: async (config) => { const { url, apiKey } = config; // Provided by your configuration @@ -56,14 +56,14 @@ module.exports = { ## Configuration -Plugins are automatically loaded and executed by the internal BullMQ job scheduler. You configure them in your `config/sso-secrets.js`: +Agents are automatically loaded and executed by the internal BullMQ job scheduler. You configure them in your `config/sso-secrets.js`: ```javascript module.exports = { // ... existing config ... discovery: { - plugins: { - my_custom_plugin: { + agents: { + my_custom_agent: { enabled: true, cron: '*/30 * * * *', // Run every 30 minutes url: 'https://api.example.com', @@ -81,8 +81,8 @@ module.exports = { ## The Reconciliation Engine -When your plugin returns its graph, the Reconciliation Engine takes over: +When your agent returns its graph, the Reconciliation Engine takes over: 1. **Matching:** It tries to find an existing device in the database matching any MAC address provided in the `interfaces` array. If no MAC matches, it falls back to IP address, and then to `slug`. -2. **Merging:** If it finds a match, it gracefully merges the metadata (so your plugin can add CPU info to a host that NMAP previously found). -3. **Source Tracking:** It records your plugin's filename in the `discovery_sources` array on the resource, and updates the `last_seen` timestamp. +2. **Merging:** If it finds a match, it gracefully merges the metadata (so your agent can add CPU info to a host that NMAP previously found). +3. **Source Tracking:** It records your agent's filename in the `discovery_sources` array on the resource, and updates the `last_seen` timestamp. 4. **LDAP Spam Prevention:** Brand new devices are marked as `managed: false`. They will not pollute your LDAP directory until an admin explicitly promotes them. diff --git a/nodejs/routes/api_conf.js b/nodejs/routes/api_conf.js index 1ba664e..217da3f 100644 --- a/nodejs/routes/api_conf.js +++ b/nodejs/routes/api_conf.js @@ -23,7 +23,16 @@ router.get('/', async (req, res) => { router.post('/', async (req, res, next) => { try { - await confManager.setVaultConf(req.body); + const existing = await confManager.getVaultConf() || {}; + // Deep merge req.body into existing + for (const key of Object.keys(req.body)) { + if (typeof req.body[key] === 'object' && req.body[key] !== null && !Array.isArray(req.body[key])) { + existing[key] = { ...(existing[key] || {}), ...req.body[key] }; + } else { + existing[key] = req.body[key]; + } + } + await confManager.setVaultConf(existing); res.json({ success: true }); } catch(err) { next(err); diff --git a/nodejs/routes/conf.js b/nodejs/routes/conf.js new file mode 100644 index 0000000..ca737c8 --- /dev/null +++ b/nodejs/routes/conf.js @@ -0,0 +1,20 @@ +const router = require('express').Router(); +const permission = require('../utils/permission'); + +router.use(async (req, res, next) => { + try { + await permission.byGroup(req.user, ['app_sso_admin']); + next(); + } catch(err) { + next(err); + } +}); + +router.get('/', (req, res) => { + res.render('conf', { + title: 'Configuration', + user: req.user + }); +}); + +module.exports = router; diff --git a/nodejs/routes/docs.js b/nodejs/routes/docs.js index 8ef2832..8cd6b9f 100644 --- a/nodejs/routes/docs.js +++ b/nodejs/routes/docs.js @@ -34,7 +34,7 @@ const DOCS = { 'oauth-apps': {title: 'Connecting Apps (SSO)', file: path.join(__dirname, '../../docs/concepts-oauth-apps.md')}, 'api-tokens': {title: 'API Tokens', file: path.join(__dirname, '../../docs/concepts-api-tokens.md')}, directory: {title: 'Directory & Inventory', file: path.join(__dirname, '../../docs/directory.md')}, - plugins: {title: 'Plugins & Scheduler', file: path.join(__dirname, '../../docs/plugins.md')}, + agents: {title: 'Agents & Scheduler', file: path.join(__dirname, '../../docs/agents.md')}, vault: {title: 'Vault Secrets', file: path.join(__dirname, '../../docs/vault.md')}, overview: {title: 'Overview', file: path.join(__dirname, '../../README.md')}, diff --git a/nodejs/routes/index.js b/nodejs/routes/index.js index 0ba5692..1dfa413 100755 --- a/nodejs/routes/index.js +++ b/nodejs/routes/index.js @@ -64,6 +64,16 @@ router.get('/notifications', (req, res) => res.redirect(301, '/overview')); router.get('/dashboard', (req, res) => res.redirect(301, '/overview')); router.get('/executive', (req, res) => res.redirect(301, '/overview')); +router.get('/conf', async function(req, res, next) { + const permission = require('../utils/permission'); + try { + await permission.byGroup(req.user, ['app_sso_admin']); + res.render('conf', {...values}); + } catch(err) { + next(err); + } +}); + router.get('/directory', function(req, res) { res.render('directory', {...values}); }); diff --git a/nodejs/views/conf.ejs b/nodejs/views/conf.ejs index 6832349..1d9f334 100644 --- a/nodejs/views/conf.ejs +++ b/nodejs/views/conf.ejs @@ -9,7 +9,25 @@ async function loadConf() { try { const data = await app.api.get('conf'); - $('#conf-json').val(JSON.stringify(data, null, 4)); + // Populate SMTP + if (data.smtp) { + $('#smtp-host').val(data.smtp.host || ''); + $('#smtp-port').val(data.smtp.port || 587); + $('#smtp-user').val(data.smtp.user || ''); + $('#smtp-pass').val(data.smtp.pass || ''); + $('#smtp-from').val(data.smtp.from || ''); + $('#smtp-secure').prop('checked', !!data.smtp.secure); + } + + // Populate OAuth + if (data.oauth) { + $('#oauth-issuer').val(data.oauth.issuer || ''); + $('#oauth-jwtsecret').val(data.oauth.jwtSecret || ''); + if (data.oauth.token_lifetime) { + $('#oauth-token-access').val(data.oauth.token_lifetime.access_token || 3600); + $('#oauth-token-refresh').val(data.oauth.token_lifetime.refresh_token || 2592000); + } + } } catch (error) { app.messages.toast('Failed to load configuration: ' + (error.message || 'Unknown error'), 'danger'); } @@ -18,50 +36,126 @@ async function saveConf() { const btn = $('#btn-save'); btn.prop('disabled', true).html(' Saving...'); + + const payload = { + smtp: { + host: $('#smtp-host').val(), + port: parseInt($('#smtp-port').val(), 10) || 587, + user: $('#smtp-user').val(), + pass: $('#smtp-pass').val(), + from: $('#smtp-from').val(), + secure: $('#smtp-secure').is(':checked') + }, + oauth: { + issuer: $('#oauth-issuer').val(), + jwtSecret: $('#oauth-jwtsecret').val(), + token_lifetime: { + access_token: parseInt($('#oauth-token-access').val(), 10) || 3600, + refresh_token: parseInt($('#oauth-token-refresh').val(), 10) || 2592000 + } + } + }; + try { - const text = $('#conf-json').val(); - const payload = JSON.parse(text); - await app.api.post('conf', payload); app.messages.toast('Configuration saved successfully! It will take effect immediately.', 'success'); } catch (error) { - let msg = error.message; - if (error instanceof SyntaxError) { - msg = 'Invalid JSON format. Please check your syntax.'; - } - app.messages.toast('Failed to save configuration: ' + msg, 'danger'); + app.messages.toast('Failed to save configuration: ' + error.message, 'danger'); } finally { btn.prop('disabled', false).html(' Save Configuration'); } } + + function togglePassword(id) { + const el = document.getElementById(id); + if (el.type === 'password') { + el.type = 'text'; + } else { + el.type = 'password'; + } + }
- Manage runtime configuration such as SMTP settings, discovery plugins, and OAuth parameters. - These secrets are stored securely in OpenBao Vault. -
++ Manage runtime configuration such as SMTP settings and OAuth parameters. + These secrets are stored securely in OpenBao Vault. +
+| Plugin Name | +Agent Name | Cron Schedule | Status | Actions | @@ -225,7 +225,7 @@
|---|---|---|---|---|
| - No plugins configured. + No agents configured. | ||||