diff --git a/CHANGELOG.md b/CHANGELOG.md index 55245fd..531c9a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# v1.33.0 - 2026-08-08 + +### Added +- **Directory Key Badges & Secret Filtering.** Added a gold `🔑 Secret` badge next to resources with stored OpenBao secrets and a `With Secrets` filter checkbox to filter the directory tree by secret presence. +- **Kind-Specific Resource Creation Modals.** Added dedicated `openAddSiteModal()`, `openAddHostModal()`, and `openAddServiceModal()` modal handlers for Site, Host, and Service resources. +- **Top Toolbar Reorganization.** Updated top tree button to **"+ Add Site"** and removed legacy `Plumbing` slider. +- **Optional Child Secret Key Name on Inheritance.** Made key name optional when inheriting parent secrets — automatically defaulting to the original parent secret key name if left blank. +- **Discovered Inventory Merge & Ignore Actions.** Added `Merge` (merge IP/interfaces/OS metadata into target resource) and `Ignore` (dismiss discovered item) endpoints (`/api/directory-admin/discovered/merge` & `/api/directory-admin/discovered/ignore`) and table action buttons. +- **Agent Tab Telemetry & Desktop Controls.** Rendered Agent Binary Version badge (`v1.8.0`), all physical disks and filesystems table, Active Logged-in Users card, and Desktop Session & Power Operations card (Lock, Display Off, Log Out, Sleep Host). + # v1.32.0 - 2026-08-08 ### Added diff --git a/nodejs/drivers/theta_agent_driver.js b/nodejs/drivers/theta_agent_driver.js index 295481c..7435a1f 100644 --- a/nodejs/drivers/theta_agent_driver.js +++ b/nodejs/drivers/theta_agent_driver.js @@ -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 diff --git a/nodejs/package.json b/nodejs/package.json index a3eff8d..78a1d44 100755 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -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": [ { diff --git a/nodejs/routes/api_directory_admin.js b/nodejs/routes/api_directory_admin.js index 229c2f4..9c7bea8 100644 --- a/nodejs/routes/api_directory_admin.js +++ b/nodejs/routes/api_directory_admin.js @@ -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; diff --git a/nodejs/views/directory.ejs b/nodejs/views/directory.ejs index 4101e7a..b18d849 100644 --- a/nodejs/views/directory.ejs +++ b/nodejs/views/directory.ejs @@ -49,10 +49,10 @@
- - + +
- + + ${optionsHtml} + + + + `, + footer: { + buttonsHtml: ` + + + ` + } + }); + } + + 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 {