@@ -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.
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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')},
|
||||
|
||||
@@ -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});
|
||||
});
|
||||
|
||||
+118
-24
@@ -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('<i class="fas fa-spinner fa-spin"></i> 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('<i class="fas fa-save"></i> Save Configuration');
|
||||
}
|
||||
}
|
||||
|
||||
function togglePassword(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (el.type === 'password') {
|
||||
el.type = 'text';
|
||||
} else {
|
||||
el.type = 'password';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container py-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h2><i class="fas fa-cogs"></i> System Configuration</h2>
|
||||
<p class="text-muted">
|
||||
Manage runtime configuration such as SMTP settings, discovery plugins, and OAuth parameters.
|
||||
These secrets are stored securely in OpenBao Vault.
|
||||
</p>
|
||||
<div class="col d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h2><i class="fas fa-cogs"></i> System Configuration</h2>
|
||||
<p class="text-muted mb-0">
|
||||
Manage runtime configuration such as SMTP settings and OAuth parameters.
|
||||
These secrets are stored securely in OpenBao Vault.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-secondary me-2" onclick="loadConf()"><i class="fas fa-undo"></i> Reset</button>
|
||||
<button id="btn-save" class="btn btn-primary" onclick="saveConf()"><i class="fas fa-save"></i> Save Configuration</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header bg-white border-bottom-0 pt-4 pb-0">
|
||||
<h5 class="mb-0">Configuration (JSON)</h5>
|
||||
<h5 class="mb-0"><i class="fas fa-envelope text-primary me-2"></i> SMTP Settings</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i> Be careful when editing this JSON! Malformed JSON will not save.
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Host</label>
|
||||
<input type="text" class="form-control" id="smtp-host">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Port</label>
|
||||
<input type="number" class="form-control" id="smtp-port">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">User</label>
|
||||
<input type="text" class="form-control" id="smtp-user">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password</label>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control" id="smtp-pass">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="togglePassword('smtp-pass')"><i class="fas fa-eye"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">From Address</label>
|
||||
<input type="text" class="form-control" id="smtp-from">
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="smtp-secure">
|
||||
<label class="form-check-label">Use Secure (TLS)</label>
|
||||
</div>
|
||||
<textarea id="conf-json" class="form-control text-monospace" rows="25" style="font-family: monospace; font-size: 14px; background-color: #f8f9fa;" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div class="card-footer bg-white border-top-0 pb-4 text-end">
|
||||
<button class="btn btn-secondary me-2" onclick="loadConf()"><i class="fas fa-undo"></i> Reset</button>
|
||||
<button id="btn-save" class="btn btn-primary" onclick="saveConf()"><i class="fas fa-save"></i> Save Configuration</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm border-0 h-100">
|
||||
<div class="card-header bg-white border-bottom-0 pt-4 pb-0">
|
||||
<h5 class="mb-0"><i class="fas fa-key text-success me-2"></i> OAuth & JWT Settings</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Issuer URL</label>
|
||||
<input type="text" class="form-control" id="oauth-issuer">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">JWT Secret</label>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control" id="oauth-jwtsecret">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="togglePassword('oauth-jwtsecret')"><i class="fas fa-eye"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Access Token Lifetime (seconds)</label>
|
||||
<input type="number" class="form-control" id="oauth-token-access">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Refresh Token Lifetime (seconds)</label>
|
||||
<input type="number" class="form-control" id="oauth-token-refresh">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+18
-12
@@ -16,7 +16,7 @@
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="plugins-tab" data-bs-toggle="tab" data-bs-target="#plugins-tab-pane" type="button" role="tab" aria-controls="plugins-tab-pane" aria-selected="false">
|
||||
<i class="fa-solid fa-plug"></i> Plugins & Scheduler
|
||||
<i class="fa-solid fa-robot"></i> Agents & Scheduler
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -186,22 +186,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plugins Tab Pane -->
|
||||
<!-- Agents Tab Pane -->
|
||||
<div class="tab-pane fade" id="plugins-tab-pane" role="tabpanel" aria-labelledby="plugins-tab">
|
||||
<div class="card shadow border-top-0">
|
||||
<div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||
<div>
|
||||
<i class="fa-solid fa-plug"></i> Plugins & Scheduler
|
||||
<i class="fa-solid fa-robot"></i> Agents & Scheduler
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 pb-0 text-muted small border-bottom">
|
||||
<i class="fa-solid fa-circle-info"></i> Manage background tasks and schedules. <a href="/docs/plugins">Learn how to make and use custom plugins</a>.
|
||||
<i class="fa-solid fa-circle-info"></i> Manage background tasks and schedules. <a href="/docs/agents">Learn how to make and use custom agents</a>.
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="card-body table table-hover mb-0 align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="ps-3">Plugin Name</th>
|
||||
<th class="ps-3">Agent Name</th>
|
||||
<th>Cron Schedule</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
@@ -225,7 +225,7 @@
|
||||
<tbody id="plugins-empty-state" style="display: none;">
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-4 text-muted">
|
||||
No plugins configured.
|
||||
No agents configured.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -1319,11 +1319,11 @@
|
||||
});
|
||||
}
|
||||
|
||||
// --- PLUGINS SCRIPTS ---
|
||||
// --- AGENT SCRIPTS ---
|
||||
function loadPlugins() {
|
||||
app.api.get('plugins', function(err, res) {
|
||||
if(err) {
|
||||
app.messages.toast("Error loading plugins: " + (err.message || err), 'danger');
|
||||
app.messages.toast("Error loading agents: " + (err.message || err), 'danger');
|
||||
return;
|
||||
}
|
||||
const plugins = res.results || {};
|
||||
@@ -1339,7 +1339,7 @@
|
||||
$.scope.plugins.push({
|
||||
name: name,
|
||||
cron: config.cron || '',
|
||||
enabled: config.enabled
|
||||
enabled: !!config.enabled
|
||||
});
|
||||
});
|
||||
$('#plugins-list').show();
|
||||
@@ -1351,14 +1351,20 @@
|
||||
function updatePlugin(name) {
|
||||
const cron = $('#cron-' + name).val();
|
||||
app.api.put('plugins/' + name, {cron: cron}, function(err, res) {
|
||||
if(err) { app.messages.toast("Failed to save: " + err.message, 'danger'); return; }
|
||||
app.messages.toast("Saved schedule successfully.", 'success');
|
||||
if(err) {
|
||||
app.messages.toast("Error saving agent schedule: " + (err.message || err), 'danger');
|
||||
return;
|
||||
}
|
||||
app.messages.toast("Agent schedule saved successfully.", 'success');
|
||||
});
|
||||
}
|
||||
|
||||
function togglePlugin(name, enable) {
|
||||
app.api.put('plugins/' + name, {enabled: enable}, function(err, res) {
|
||||
if(err) { app.messages.toast("Failed to toggle: " + err.message, 'danger'); return; }
|
||||
if(err) {
|
||||
app.messages.toast("Error toggling agent: " + (err.message || err), 'danger');
|
||||
return;
|
||||
}
|
||||
loadPlugins();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user