release: v1.33.0 - Directory Key Badges, Discovered Inventory Merge/Ignore & Desktop Operations

This commit is contained in:
2026-08-08 18:12:37 -04:00
parent fc9952add3
commit 620f401091
5 changed files with 246 additions and 26 deletions
+13 -1
View File
@@ -42,12 +42,14 @@ class ThetaAgentDriver extends BaseDriver {
status: 'online',
driver: this.name,
agentId: agent.id,
agentVersion: agent.version,
agentVersion: agent.version || 'v1.7.0',
lastSeen: agent.lastSeen,
system: {
cpu: telemetry.cpu || null,
ram: telemetry.memory || null,
disk: telemetry.disk || null,
disks: telemetry.disks || [],
loggedUsers: telemetry.loggedUsers || [],
uptime: telemetry.uptime || null
}
};
@@ -82,6 +84,16 @@ class ThetaAgentDriver extends BaseDriver {
return { status: 'ok', driver: this.name, action, result };
}
if (['desktop_control', 'lock_session', 'logout_user', 'display_off', 'sleep_host'].includes(action) || subType.startsWith('desktop')) {
const subAction = params.subAction || action;
const targetUser = params.user || '';
const result = await AgentManager.sendCommand(agent.id, 'desktop_control', {
subAction,
user: targetUser
});
return { status: 'ok', driver: this.name, action: subAction, result };
}
if (action === 'systemd_action' || subType === 'systemd') {
const serviceName = params.serviceName || (resource.metadata && resource.metadata.systemdService) || resource.slug;
const subAction = params.subAction || action; // start, stop, restart, reload
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "t42-sso-manager",
"version": "1.32.0",
"version": "1.33.0",
"description": "A very simple LDAP management and SSO system",
"author": [
{
+57
View File
@@ -799,4 +799,61 @@ router.get('/resources/:id/driver-logs', async (req, res, next) => {
} catch (err) { next(err); }
});
// ── Discovered Inventory Operations (Merge & Ignore) ───────────────────────
router.post('/discovered/ignore', async (req, res, next) => {
try {
const { resourceId } = req.body;
if (!resourceId) return res.status(400).json({ status: 'error', message: 'resourceId is required' });
const r = await Resource.get(resourceId);
if (!r) return res.status(404).json({ status: 'error', message: 'resource not found' });
r.metadata = r.metadata || {};
r.metadata.ignored = true;
await r.save();
res.json({ status: 'ok', resourceId: r.id, ignored: true });
} catch (err) { next(err); }
});
router.post('/discovered/merge', async (req, res, next) => {
try {
const { discoveredId, targetId } = req.body;
if (!discoveredId || !targetId) {
return res.status(400).json({ status: 'error', message: 'discoveredId and targetId are required' });
}
const disc = await Resource.get(discoveredId);
const target = await Resource.get(targetId);
if (!disc || !target) {
return res.status(404).json({ status: 'error', message: 'Discovered or Target resource not found' });
}
// Merge metadata (interfaces, discovery sources, OS details)
target.metadata = target.metadata || {};
disc.metadata = disc.metadata || {};
const sources = new Set([...(target.metadata.discovery_sources || []), ...(disc.metadata.discovery_sources || [])]);
target.metadata.discovery_sources = Array.from(sources);
if (disc.metadata.interfaces) {
const existingInterfaces = target.metadata.interfaces || [];
const macs = new Set(existingInterfaces.map(i => i.mac).filter(Boolean));
for (const iface of disc.metadata.interfaces) {
if (!iface.mac || !macs.has(iface.mac)) {
existingInterfaces.push(iface);
}
}
target.metadata.interfaces = existingInterfaces;
}
if (disc.metadata.os) target.metadata.os = target.metadata.os || disc.metadata.os;
if (disc.metadata.kernel) target.metadata.kernel = target.metadata.kernel || disc.metadata.kernel;
await target.save();
// Remove or mark discovered record as merged
await disc.delete();
res.json({ status: 'ok', mergedTargetId: target.id, targetName: target.name });
} catch (err) { next(err); }
});
module.exports = router;
+165 -24
View File
@@ -49,10 +49,10 @@
<button type="button" class="btn btn-outline-secondary" onclick="collapseAllTree()" title="Collapse all"><i class="fa-solid fa-angles-up"></i></button>
</div>
<div class="form-check form-switch form-check-inline ms-1 me-1">
<input class="form-check-input" type="checkbox" id="toggle-plumbing" onchange="renderTable()">
<label class="form-check-label small text-muted" for="toggle-plumbing" title="Show containers, oauth clients, and sidecars">Plumbing</label>
<input class="form-check-input" type="checkbox" id="toggle-secrets-only" onchange="renderTable()">
<label class="form-check-label small text-muted" for="toggle-secrets-only" title="Filter tree to show only resources with stored secrets"><i class="fa-solid fa-key text-warning me-1"></i>With Secrets</label>
</div>
<input type="text" id="search-filter" class="form-control form-control-sm shadow-sm" placeholder="Search..." onkeyup="renderTable()" style="width: 200px;">
<input type="text" id="search-filter" class="form-control form-control-sm shadow-sm" placeholder="Search resources or keys..." onkeyup="renderTable()" style="width: 210px;">
<select id="sort-by" class="form-select form-select-sm shadow-sm" onchange="renderTable()" style="width: 150px;">
<option value="name">Name (A-Z)</option>
<option value="kind">Kind</option>
@@ -68,8 +68,8 @@
<button class="btn btn-sm btn-outline-primary ms-1 shadow-sm" onclick="openAgentInstallModal()">
<i class="fa-solid fa-shield-halved me-1"></i> Install Agent
</button>
<button class="btn btn-sm btn-primary ms-1 shadow-sm" onclick="openAddModal()">
<i class="fas fa-plus"></i> Add Resource
<button class="btn btn-sm btn-primary ms-1 shadow-sm" onclick="openAddSiteModal()">
<i class="fas fa-plus"></i> Add Site
</button>
</div>
</div>
@@ -98,6 +98,7 @@
<strong>{{name}}</strong>
</a>
<span class="badge bg-secondary me-1">{{kind}}{{#metadata.subType}} ({{metadata.subType}}){{/metadata.subType}}</span>
{{#hasSecret}}<span class="badge bg-warning text-dark me-1" title="Has stored secrets in OpenBao"><i class="fa-solid fa-key me-1"></i>Secret</span>{{/hasSecret}}
{{#metadata.isProduction}}<span class="badge bg-danger me-1">Prod</span>{{/metadata.isProduction}}
{{^metadata.isProduction}}<span class="badge bg-info me-1">Dev</span>{{/metadata.isProduction}}
</td>
@@ -206,9 +207,15 @@
</td>
<td class="text-end pe-3">
{{^metadata.managed}}
<button class="btn btn-sm btn-outline-primary" onclick="promoteResource('{{slug}}')" title="Promote to Managed">
<button class="btn btn-sm btn-outline-primary me-1" onclick="promoteResource('{{slug}}')" title="Promote to Managed Directory Host">
<i class="fa-solid fa-arrow-up-right-dots"></i> Promote
</button>
<button class="btn btn-sm btn-outline-success me-1" onclick="openMergeModal('{{id}}', '{{name}}')" title="Merge into Existing Resource">
<i class="fa-solid fa-code-merge"></i> Merge
</button>
<button class="btn btn-sm btn-outline-danger" onclick="ignoreDiscoveredResource('{{id}}')" title="Ignore / Dismiss">
<i class="fa-solid fa-eye-slash"></i> Ignore
</button>
{{/metadata.managed}}
{{#metadata.managed}}
<button class="btn btn-sm btn-outline-secondary" disabled title="Already Managed">
@@ -809,9 +816,29 @@
disksHtml = `<div class="small">Disk Usage: <strong>${fmtNum(t.disk_usage_percent ?? 0)}%</strong> ${bar(t.disk_usage_percent)}</div>`;
}
const agentVer = d.version || t.version || agent.version || 'v1.7.0';
const loggedUsers = t.logged_users || d.logged_users || agent.loggedUsers || [];
let loggedUsersHtml = '';
if (loggedUsers.length > 0) {
loggedUsersHtml = `<div class="table-responsive"><table class="table table-sm small mb-0 align-middle">
<thead><tr><th>User</th><th>Terminal</th><th>Host / IP</th><th>Session Time</th></tr></thead><tbody>` +
loggedUsers.map(u => `<tr>
<td><i class="fa-solid fa-user text-primary me-1"></i><strong>${esc(u.user)}</strong></td>
<td><code class="text-muted">${esc(u.terminal || 'tty')}</code></td>
<td>${esc(u.host || 'local')}</td>
<td class="text-muted">${u.started ? moment(u.started * 1000).fromNow() : 'active'}</td>
</tr>`).join('') + `</tbody></table></div>`;
} else {
loggedUsersHtml = `<div class="small text-muted p-2"><i class="fa-solid fa-user-slash me-1"></i>No active logged-in user sessions.</div>`;
}
return `<div class="p-3">
<div class="mb-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0">${esc(agent.name || agent.hostname || 'unknown')} ${online}</h5>
<h5 class="mb-0">
${esc(agent.name || agent.hostname || 'unknown')} ${online}
<span class="badge bg-dark ms-2 font-monospace" title="Theta Agent Binary Version"><i class="fa-solid fa-code-branch me-1"></i>${esc(agentVer)}</span>
</h5>
<small class="text-muted">Last seen ${lastSeenStr}</small>
</div>
@@ -856,18 +883,41 @@
<!-- Disks Card -->
<div class="card mb-3 shadow-sm border">
<div class="card-header bg-light py-2">
<h6 class="mb-0 fw-bold text-dark"><i class="fa-solid fa-hard-drive me-1"></i> Disks & Storage</h6>
<h6 class="mb-0 fw-bold text-dark"><i class="fa-solid fa-hard-drive me-1"></i> Physical Disks & Filesystems</h6>
</div>
<div class="card-body p-2">
${disksHtml}
</div>
</div>
<!-- Logged-in Users Card -->
<div class="card mb-3 shadow-sm border">
<div class="card-header bg-light py-2">
<h6 class="mb-0 fw-bold text-dark"><i class="fa-solid fa-users me-1"></i> Active Logged-in Users</h6>
</div>
<div class="card-body p-2">
${loggedUsersHtml}
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-6">GPU <strong>${gpu}</strong></div>
<div class="col-6">ZFS <strong>${esc(t.zfs_health || 'N/A')}</strong></div>
</div>
<!-- Desktop Controls Card -->
<div class="card mb-3 border-info shadow-sm">
<div class="card-header bg-info text-dark py-2">
<h6 class="mb-0 fw-bold"><i class="fa-solid fa-desktop me-1"></i> Desktop Session & Power Controls</h6>
</div>
<div class="card-body p-3 d-flex flex-wrap gap-2">
<button class="btn btn-outline-dark btn-sm" onclick="agentDesktopControl('${agent.id}', 'lock')"><i class="fa-solid fa-lock me-1"></i> Lock Session</button>
<button class="btn btn-outline-secondary btn-sm" onclick="agentDesktopControl('${agent.id}', 'display_off')"><i class="fa-solid fa-tv me-1"></i> Display Off</button>
<button class="btn btn-outline-warning btn-sm" onclick="agentDesktopControl('${agent.id}', 'logout')"><i class="fa-solid fa-right-from-bracket me-1"></i> Log Out User</button>
<button class="btn btn-outline-primary btn-sm" onclick="agentDesktopControl('${agent.id}', 'sleep')"><i class="fa-solid fa-moon me-1"></i> Sleep Host</button>
</div>
</div>
<!-- Host Power Controls -->
<div class="card mb-3 border-danger shadow-sm">
<div class="card-header bg-danger text-white py-2">
@@ -1041,22 +1091,21 @@
function renderTable() {
const filter = $('#search-filter').val().toLowerCase();
const sort = $('#sort-by').val();
const showPlumbing = $('#toggle-plumbing').is(':checked');
const secretsOnly = $('#toggle-secrets-only').is(':checked');
let filtered = rawResources.filter(r => {
if (!showPlumbing && !filter) {
const sub = (r.metadata?.subType || '').toLowerCase();
if (r.kind === 'container' || r.kind === 'oauth' || sub === 'sidecar' || sub === 'container' || sub === 'openresty') {
return false;
}
if (secretsOnly && !r.hasSecret) {
return false;
}
if (!filter) return true;
const secretKeysStr = (r.secretKeys || []).join(' ').toLowerCase();
return (r.name || '').toLowerCase().includes(filter) ||
(r.slug || '').toLowerCase().includes(filter) ||
(r.kind || '').toLowerCase().includes(filter) ||
(r.metadata?.subType || '').toLowerCase().includes(filter) ||
(r.metadata?.ip || '').toLowerCase().includes(filter) ||
(r.hostName || '').toLowerCase().includes(filter);
(r.hostName || '').toLowerCase().includes(filter) ||
secretKeysStr.includes(filter);
});
// Sort
@@ -1352,6 +1401,96 @@
populateHostDropdown(parentId || '');
toggleFormFields();
}
function openAddSiteModal() {
openAddModal(null, null);
$('#res-kind').val('site');
toggleFormFields();
}
function openAddHostModal(parentId) {
openAddModal(parentId, 'site');
$('#res-kind').val('host');
toggleFormFields();
}
function openAddServiceModal(parentId) {
openAddModal(parentId, 'host');
$('#res-kind').val('service');
toggleFormFields();
}
async function openMergeModal(discoveredId, discName) {
const targets = rawResources.filter(r => r.kind === 'host' || r.kind === 'site' || r.kind === 'service');
let optionsHtml = targets.map(r => `<option value="${r.id}">${esc(r.name)} (${esc(r.kind)}${r.metadata?.subType ? ' - ' + esc(r.metadata.subType) : ''})</option>`).join('');
app.modal.open({
title: `Merge '${esc(discName)}' into Existing Resource`,
size: 'md',
bodyHtml: `
<div class="p-3">
<p class="small text-muted">Select an existing Directory resource to merge IP addresses, network interfaces, and OS telemetry into:</p>
<div class="mb-3">
<label class="form-label font-weight-bold">Target Directory Resource</label>
<select class="form-select" id="merge-target-id">
${optionsHtml}
</select>
</div>
</div>
`,
footer: {
buttonsHtml: `
<button class="btn btn-secondary btn-sm" onclick="app.modal.close()">Cancel</button>
<button class="btn btn-success btn-sm" onclick="submitMergeResource('${discoveredId}')"><i class="fa-solid fa-code-merge me-1"></i> Confirm Merge</button>
`
}
});
}
async function submitMergeResource(discoveredId) {
const targetId = $('#merge-target-id').val();
if (!targetId) return;
try {
app.messages.action('Merging discovered resource...', $('#app-modal-body'), 'info');
await app.api.post('directory-admin/discovered/merge', { discoveredId, targetId });
app.messages.action('Resource merged successfully!', null, 'success');
app.modal.close();
loadData();
} catch (err) {
app.messages.action(err.message || 'Failed to merge resource', $('#app-modal-body'), 'danger');
}
}
async function ignoreDiscoveredResource(resourceId) {
const confirmed = await app.messages.confirm('Ignore and dismiss this discovered device?', null, 'warning');
if (!confirmed) return;
try {
await app.api.post('directory-admin/discovered/ignore', { resourceId });
app.messages.action('Discovered device ignored.', null, 'info');
loadData();
} catch (err) {
app.messages.action(err.message || 'Failed to ignore device', null, 'danger');
}
}
async function agentDesktopControl(agentId, action, username = '') {
const confirmed = await app.messages.confirm(`Execute '${action}' desktop control action?`, $('#res-modal'), 'warning');
if (!confirmed) return;
const resourceId = $('#res-id').val();
if (!resourceId) return;
try {
app.messages.action(`Sending '${action}' desktop command to agent...`, $('#res-modal'), 'info');
const res = await app.api.post(`directory-admin/resources/${resourceId}/driver-action`, {
action: 'desktop_control',
subAction: action,
user: username
});
app.messages.action(`Desktop action '${action}' completed successfully!`, $('#res-modal'), 'success');
} catch (err) {
app.messages.action(err.message || 'Desktop action failed', $('#res-modal'), 'danger');
}
}
var ldapGroupsCache = null;
async function loadLdapGroups() {
@@ -1753,22 +1892,24 @@
}
async function inheritParentSecret() {
const childKey = $('#inherit-child-key').val().trim();
let childKey = $('#inherit-child-key').val().trim();
const inheritVal = $('#inherit-parent-select').val();
const resourceId = $('#res-id').val();
if (!childKey) {
app.messages.action('Please enter a child secret key name (e.g. DB_HOST).', $('#secrets-tab-container'), 'warning');
return;
}
if (!SECRET_KEY_REGEX.test(childKey)) {
app.messages.action('Invalid child key name. Only letters, numbers, and underscores allowed.', $('#secrets-tab-container'), 'danger');
return;
}
if (!inheritVal) {
app.messages.action('Select a parent secret to inherit from.', $('#secrets-tab-container'), 'warning');
return;
}
if (!childKey) {
const parts = inheritVal.split(':');
childKey = parts[parts.length - 1];
}
if (!SECRET_KEY_REGEX.test(childKey)) {
app.messages.action('Invalid child key name. Only letters, numbers, and underscores allowed.', $('#secrets-tab-container'), 'danger');
return;
}
if (!resourceId) return;
try {