Merge pull request #147 from theta42/fix-68
fix: enforce pwdAccountLockedTime check in app and LDAP
This commit is contained in:
Binary file not shown.
@@ -58,6 +58,7 @@ class PluginInstance extends Model {
|
||||
lastRunAt: { type: 'integer' },
|
||||
lastStatus: { type: 'string' },
|
||||
lastError: { type: 'text' },
|
||||
lastLog: { type: 'text' },
|
||||
// Audit stamps (set by the route handler, not by an ORM hook).
|
||||
created_by: { type: 'string' },
|
||||
created_on: { type: 'integer' },
|
||||
|
||||
@@ -96,11 +96,13 @@ class Resource extends Model {
|
||||
return false;
|
||||
}
|
||||
|
||||
let maxUpdated = 0;
|
||||
resObjs.forEach(r => {
|
||||
r.metadata.isProduction = checkProd(r.id);
|
||||
if (r.updated_on && r.updated_on > maxUpdated) maxUpdated = r.updated_on;
|
||||
});
|
||||
|
||||
return { resources: resObjs, edges };
|
||||
return { resources: resObjs, edges, updated_on: maxUpdated || Date.now() };
|
||||
}
|
||||
|
||||
// Stamp `resolvedAddress` on each resource: its own address/ip if it has one,
|
||||
|
||||
@@ -773,7 +773,7 @@ User.setActive = async function(active) {
|
||||
]);
|
||||
} else {
|
||||
await client.modify(this.dn, [
|
||||
new Change({ operation: 'replace', modification: new Attribute({ type: 'pwdAccountLockedTime', values: ['000001010000Z'] }) }),
|
||||
new Change({ operation: 'replace', modification: new Attribute({ type: 'pwdAccountLockedTime', values: ['00000101000000Z'] }) }),
|
||||
]);
|
||||
}
|
||||
});
|
||||
@@ -788,7 +788,7 @@ User.setActive = async function(active) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
this.pwdAccountLockedTime = active ? undefined : '000001010000Z';
|
||||
this.pwdAccountLockedTime = active ? undefined : '00000101000000Z';
|
||||
this.isActive = active ? 'active' : '';
|
||||
this.isInactive = active ? '' : 'inactive';
|
||||
cache.clear();
|
||||
@@ -907,6 +907,13 @@ User.login = async function(data){
|
||||
}
|
||||
let user = await this.get(data.uid || data.username);
|
||||
|
||||
if (user.pwdAccountLockedTime) {
|
||||
let error = new Error('Invalid Credentials, login failed.');
|
||||
error.name = 'LDAPLoginFailed';
|
||||
error.status = 401;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const loginClient = makeClient();
|
||||
try {
|
||||
await loginClient.bind(user.dn, data.password);
|
||||
|
||||
@@ -34,7 +34,13 @@ module.exports = {
|
||||
// OsAndPortScan requires root (for -O). NmapScan does a basic port scan (TCP connect if non-root).
|
||||
const scan = new nmap.NmapScan(targetRange);
|
||||
scan.command.push('-Pn');
|
||||
scan.command.push('-F'); // fast scan, 100 top ports
|
||||
scan.command.push('--min-rate', '100'); // speed up the scan
|
||||
|
||||
if (config.log) config.log(`Starting nmap scan: ${scan.command.join(' ')}`);
|
||||
|
||||
scan.on('complete', function(data) {
|
||||
if (config.log) config.log(`Scan complete. Found ${data ? data.length : 0} hosts.`);
|
||||
const resources = [];
|
||||
const edges = [];
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
discover: async (config) => {
|
||||
const { url, tokenId, tokenSecret } = config;
|
||||
let { url, tokenId, tokenSecret } = config;
|
||||
if (!url || !tokenId || !tokenSecret) {
|
||||
throw new Error("Missing Proxmox config");
|
||||
}
|
||||
|
||||
@@ -201,6 +201,16 @@ router.post('/resources/:id/rotate-secret', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/resources/:id/service-token', async (req, res, next) => {
|
||||
try {
|
||||
const { ServiceToken } = require('../models/token');
|
||||
const token = await ServiceToken.issue(req.params.id, req.user.uid);
|
||||
res.json({ results: { token: token.token } });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/resources/:id', async (req, res, next) => {
|
||||
try {
|
||||
const r = await Resource.get(req.params.id);
|
||||
|
||||
@@ -274,7 +274,7 @@ router.get('/:id/runs', async (req, res, next) => {
|
||||
try {
|
||||
const inst = await PluginInstance.get(req.params.id);
|
||||
if (!inst) return res.status(404).json({ error: 'Not found' });
|
||||
res.json({ results: { lastRunAt: inst.lastRunAt, lastStatus: inst.lastStatus, lastError: inst.lastError } });
|
||||
res.json({ results: { lastRunAt: inst.lastRunAt, lastStatus: inst.lastStatus, lastError: inst.lastError, lastLog: inst.lastLog } });
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ router.get('/graph', async (req, res, next) => {
|
||||
res.json(envelope({
|
||||
resources: projectResources(graph.resources, { fullMetadata }),
|
||||
edges: graph.edges,
|
||||
updated_on: graph.updated_on
|
||||
}));
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
@@ -98,6 +99,42 @@ router.get('/me', async (req, res, next) => {
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
// GET /api/discovery/access/:uid[/:slug]
|
||||
// Answers per-user access for a machine caller (e.g. jump-host).
|
||||
router.get(['/access/:uid', '/access/:uid/:slug'], async (req, res, next) => {
|
||||
try {
|
||||
const { fullMetadata } = await callerView(req);
|
||||
if (!req.user || (!req.user.isMachine && !fullMetadata)) {
|
||||
return res.status(403).json(envelope({ error: 'Only machine identities or admins may query access for other users.' }));
|
||||
}
|
||||
const { User } = require('../models/user_ldap');
|
||||
const { groupCns } = require('../utils/user_groups');
|
||||
|
||||
const targetUser = await User.get(req.params.uid).catch(() => null);
|
||||
if (!targetUser) return res.status(404).json(envelope({ error: 'User not found' }));
|
||||
|
||||
const groups = await groupCns(targetUser);
|
||||
const ids = new Set();
|
||||
if (groups.length) {
|
||||
const rgs = await ResourceGroup.list({ where: { groupCn: { in: groups } } });
|
||||
for (const rg of rgs) ids.add(rg.resourceId);
|
||||
}
|
||||
|
||||
let all = await Resource.list();
|
||||
if (req.params.slug) all = all.filter(r => r.slug === req.params.slug);
|
||||
|
||||
let accessible = all.filter(r => {
|
||||
const isAuto = r.metadata?.discovery_sources?.length > 0 && !r.metadata.discovery_sources.includes('manual');
|
||||
const isManaged = r.metadata?.managed === true;
|
||||
if (isAuto && !isManaged) return false;
|
||||
return ids.has(r.id) || (r.metadata && r.metadata.isPublic);
|
||||
});
|
||||
|
||||
accessible = await Resource.withResolvedAddress(accessible);
|
||||
res.json(envelope(projectResources(accessible, { fullMetadata })));
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
// POST /api/discovery/sync
|
||||
// Used by external agents (e.g. ldap-client) to push discovery data.
|
||||
router.post('/sync', async (req, res, next) => {
|
||||
|
||||
@@ -90,7 +90,8 @@ router.get('/plugins', function(req, res, next) {
|
||||
// 'app_sso_directory_admin','admin']) and the /api/plugins endpoints enforce
|
||||
// the same server-side. Same header-vs-navigation auth model as /conf and
|
||||
// /vault (auth-token is a client-set header, not a cookie).
|
||||
res.render('plugins', {...values});
|
||||
const registry = require('../services/plugin_registry');
|
||||
res.render('plugins', {...values, pluginTypes: registry.types });
|
||||
});
|
||||
|
||||
router.get('/vault', function(req, res) {
|
||||
|
||||
@@ -71,17 +71,23 @@ async function runPluginJob(instanceId) {
|
||||
}
|
||||
|
||||
console.log(`[Scheduler] Running plugin: ${instance.slug} (${instance.pluginType})`);
|
||||
await instance.update({ lastRunAt: Date.now(), lastStatus: STATUS.RUNNING, lastError: null });
|
||||
await instance.update({ lastRunAt: Date.now(), lastStatus: STATUS.RUNNING, lastError: null, lastLog: null });
|
||||
let logs = [];
|
||||
try {
|
||||
const cfg = await pluginSecrets.mergeForRun(instance);
|
||||
cfg.log = (msg) => {
|
||||
logs.push(`[${new Date().toISOString()}] ${msg}`);
|
||||
console.log(`[Plugin ${instance.slug}] ${msg}`);
|
||||
if (logs.length > 1000) logs.shift();
|
||||
};
|
||||
const payload = await runFn(cfg);
|
||||
if (instance.category === 'discovery') {
|
||||
await DiscoveryReconciler.reconcile(instance.slug, payload);
|
||||
}
|
||||
await instance.update({ lastStatus: STATUS.OK, lastError: null });
|
||||
await instance.update({ lastStatus: STATUS.OK, lastError: null, lastLog: logs.join('\n') });
|
||||
} catch (err) {
|
||||
console.error(`[Scheduler] Plugin ${instance.slug} failed:`, err.message);
|
||||
await instance.update({ lastStatus: STATUS.ERROR, lastError: String(err.message || err) });
|
||||
await instance.update({ lastStatus: STATUS.ERROR, lastError: String(err.message || err), lastLog: logs.join('\n') });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,11 +21,6 @@
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
<input type="text" id="search-filter" class="form-control form-control-sm shadow-sm" placeholder="Search resources..." onkeyup="renderTable()" style="width: 250px;">
|
||||
<select id="filter-managed" class="form-select form-select-sm shadow-sm" onchange="renderTable()" style="width: 150px;">
|
||||
<option value="all">All Resources</option>
|
||||
<option value="unmanaged" selected>Unmanaged Only</option>
|
||||
<option value="managed">Managed Only</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-header actionMessage" style="display:none"></div>
|
||||
@@ -132,10 +127,9 @@
|
||||
// Name search
|
||||
if(search && !r.name.toLowerCase().includes(search) && !r.slug.toLowerCase().includes(search)) return false;
|
||||
|
||||
// Managed filter
|
||||
// Always hide items that have been committed to the catalog (managed)
|
||||
const isManaged = !!(r.metadata && r.metadata.managed);
|
||||
if(managedFilter === 'managed' && !isManaged) return false;
|
||||
if(managedFilter === 'unmanaged' && isManaged) return false;
|
||||
if(isManaged) return false;
|
||||
|
||||
const isAuto = r.metadata && r.metadata.discovery_sources && r.metadata.discovery_sources.length > 0 && !r.metadata.discovery_sources.includes('manual');
|
||||
if(!isAuto) return false;
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<button class="btn btn-sm btn-warning" title="Edit Secrets" onclick="openSecretsModal('{{id}}')"><i class="fa-solid fa-key"></i></button>
|
||||
<button class="btn btn-sm btn-info" title="Test" onclick="testPlugin('{{id}}')"><i class="fa-solid fa-vial"></i></button>
|
||||
<button class="btn btn-sm btn-success" title="Run now" onclick="runNow('{{id}}')"><i class="fa-solid fa-play"></i></button>
|
||||
{{#lastError}}<button class="btn btn-sm btn-secondary" title="View Logs" onclick="showLogs('{{id}}')"><i class="fa-solid fa-file-lines"></i></button>{{/lastError}}
|
||||
{{#lastRunAt}}<button class="btn btn-sm btn-secondary" title="View Logs" onclick="showLogs('{{id}}')"><i class="fa-solid fa-file-lines"></i></button>{{/lastRunAt}}
|
||||
{{#enabled}}<button class="btn btn-sm btn-outline-danger" title="Unload" onclick="togglePlugin('{{id}}', false)">Unload</button>{{/enabled}}
|
||||
{{^enabled}}<button class="btn btn-sm btn-outline-success" title="Load" onclick="togglePlugin('{{id}}', true)">Load</button>{{/enabled}}
|
||||
<button class="btn btn-sm btn-outline-danger" title="Delete" onclick="deletePlugin('{{id}}')"><i class="fa-solid fa-trash"></i></button>
|
||||
@@ -370,14 +370,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
function showLogs(id) {
|
||||
async function showLogs(id) {
|
||||
var p = pluginsById[id];
|
||||
if (!p || !p.lastError) return;
|
||||
if (!p) return;
|
||||
try {
|
||||
const res = await app.api.get('plugins/' + id + '/runs');
|
||||
const logText = (res.results && res.results.lastLog) || (res.results && res.results.lastError) || 'No logs available.';
|
||||
app.modal.open({
|
||||
title: 'Logs — ' + p.name,
|
||||
bodyHtml: '<pre class="bg-dark text-white p-3 rounded" style="white-space: pre-wrap; font-size: 0.85em;">' + String(p.lastError).replace(/</g, '<').replace(/>/g, '>') + '</pre>',
|
||||
bodyHtml: '<pre class="bg-dark text-white p-3 rounded" style="white-space: pre-wrap; font-size: 0.85em;">' + String(logText).replace(/</g, '<').replace(/>/g, '>') + '</pre>',
|
||||
footer: { buttonsHtml: '<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>' }
|
||||
});
|
||||
} catch (err) {
|
||||
app.messages.toast('Failed to load logs: ' + (err.message || err), 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async function togglePlugin(id, enable) {
|
||||
|
||||
Reference in New Issue
Block a user