feat: release v1.31.0 with Zero-View secrets engine, generator, multi-level inheritance, and SSSD key mappings

This commit is contained in:
2026-08-07 23:26:39 -04:00
parent 181ca8c9cb
commit 15d9ce1078
10 changed files with 740 additions and 52 deletions
+286 -5
View File
@@ -48,6 +48,10 @@
<button type="button" class="btn btn-outline-secondary" onclick="expandAllTree()" title="Expand all"><i class="fa-solid fa-angles-down"></i></button>
<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>
</div>
<input type="text" id="search-filter" class="form-control form-control-sm shadow-sm" placeholder="Search..." onkeyup="renderTable()" style="width: 200px;">
<select id="sort-by" class="form-select form-select-sm shadow-sm" onchange="renderTable()" style="width: 150px;">
<option value="name">Name (A-Z)</option>
@@ -90,12 +94,12 @@
{{{indentHtml}}}
{{{caretHtml}}}
{{#isHost}}<span class="d-inline-block rounded-circle me-1" style="width:10px;height:10px;background:{{agentColor}};" title="{{agentStatusTitle}}"></span>{{/isHost}}
<span class="badge bg-secondary">{{kind}}{{#metadata.subType}} ({{metadata.subType}}){{/metadata.subType}}</span>
{{#metadata.isProduction}}<span class="badge bg-danger">Prod</span>{{/metadata.isProduction}}
{{^metadata.isProduction}}<span class="badge bg-info">Dev</span>{{/metadata.isProduction}}
<a href="#" class="text-reset text-decoration-none ms-2" onclick="openEditModal('{{id}}'); return false;" title="View details">
<a href="#" class="text-reset text-decoration-none me-2" onclick="openEditModal('{{id}}'); return false;" title="View details">
<strong>{{name}}</strong>
</a>
<span class="badge bg-secondary me-1">{{kind}}{{#metadata.subType}} ({{metadata.subType}}){{/metadata.subType}}</span>
{{#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>
<td>
{{#metadata.ip}}<div><small>IP:</small> {{metadata.ip}}</div>{{/metadata.ip}}
@@ -507,6 +511,95 @@
</div>
`;
var secretsTabHtml = `
<div class="mb-3" id="secrets-tab-container">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h6 class="mb-0"><i class="fa-solid fa-vault text-warning me-2"></i>Resource Secrets (OpenBao KV Engine)</h6>
<small class="text-muted">Encrypted key-value secrets stored in OpenBao under <code>secret/data/resources/&lt;slug&gt;/conf</code></small>
</div>
<button class="btn btn-sm btn-outline-primary" onclick="refreshResourceSecrets()"><i class="fa-solid fa-sync me-1"></i> Refresh</button>
</div>
<div class="secrets-action-msg mb-2" style="display:none"></div>
<div class="table-responsive shadow-sm rounded border mb-3">
<table class="table table-hover align-middle mb-0" id="secrets-table">
<thead class="table-dark">
<tr>
<th style="width: 35%;">Secret Key</th>
<th>Status / Security</th>
<th style="width: 240px;" class="text-end">Actions</th>
</tr>
</thead>
<tbody id="secrets-table-body">
<tr><td colspan="3" class="text-center text-muted py-3">Loading secrets...</td></tr>
</tbody>
</table>
</div>
<!-- Add / Generate Secret Card -->
<div class="card bg-light border-0 shadow-sm p-3 mb-3">
<h6 class="card-title text-dark mb-2"><i class="fa-solid fa-plus-circle text-primary me-1"></i> Add or Generate Secret Key</h6>
<div class="row g-2 align-items-center mb-2">
<div class="col-md-5">
<label class="form-label small text-muted mb-1">Secret Key Name (e.g. <code>DB_PASSWORD</code>)</label>
<input type="text" class="form-control form-control-sm font-monospace" id="new-secret-key" placeholder="DB_PASSWORD" onkeyup="validateSecretKeyInput(this)">
<div class="invalid-feedback small">Only letters, numbers, and underscores allowed (e.g. DB_PASSWORD).</div>
</div>
<div class="col-md-4">
<label class="form-label small text-muted mb-1">Secret Value</label>
<input type="text" class="form-control form-control-sm font-monospace" id="new-secret-val" placeholder="Enter value or click Generate">
</div>
<div class="col-md-3 pt-3">
<button class="btn btn-sm btn-primary w-100" id="btn-add-secret" onclick="addSecretRow()"><i class="fa-solid fa-save me-1"></i> Save Secret</button>
</div>
</div>
<div class="row g-2 align-items-center mt-1">
<div class="col-md-4">
<label class="form-label small text-muted mb-1">Generator Length</label>
<select class="form-select form-select-sm" id="gen-secret-length">
<option value="8">8 characters</option>
<option value="12">12 characters</option>
<option value="16">16 characters</option>
<option value="24">24 characters</option>
<option value="32" selected>32 characters (Default)</option>
<option value="48">48 characters</option>
<option value="64">64 characters</option>
<option value="128">128 characters</option>
</select>
</div>
<div class="col-md-8 text-end pt-3">
<button class="btn btn-sm btn-outline-success" onclick="generateSecretValue()"><i class="fa-solid fa-bolt me-1"></i> Generate Secret into Field</button>
</div>
</div>
<div class="alert alert-warning border-0 shadow-sm p-2 small mt-2 mb-0" id="gen-secret-notice" style="display:none">
<i class="fa-solid fa-shield-halved text-warning me-1"></i> Generated secret is shown in the field above. Click <strong>Save Secret</strong> to store in OpenBao — secret values will not be displayed again once saved.
</div>
</div>
<!-- Inherit Parent Secret Card -->
<div class="card bg-light border-0 shadow-sm p-3" id="inherit-secret-card" style="display:none">
<h6 class="card-title text-dark mb-2"><i class="fa-solid fa-diagram-project text-info me-1"></i> Inherit Secret from Parent Resource</h6>
<div class="row g-2 align-items-center">
<div class="col-md-4">
<label class="form-label small text-muted mb-1">Child Secret Key Name</label>
<input type="text" class="form-control form-control-sm font-monospace" id="inherit-child-key" placeholder="DB_HOST">
</div>
<div class="col-md-5">
<label class="form-label small text-muted mb-1">Parent Resource Secret</label>
<select class="form-select form-select-sm" id="inherit-parent-select"></select>
</div>
<div class="col-md-3 pt-3">
<button class="btn btn-sm btn-outline-info w-100" onclick="inheritParentSecret()"><i class="fa-solid fa-link me-1"></i> Inherit Secret</button>
</div>
</div>
</div>
</div>
`;
// Shared by openAddModal/openEditModal: builds the tabbed/footer/(optionally
// URL-tracked) modal DOM. Callers then populate fields via .val() and hide
// the Groups/Children tabs in add-mode (no resource id to scope them to).
@@ -519,6 +612,7 @@
{id: 'details', label: 'Details', bodyHtml: detailsTabHtml},
{id: 'groups', label: 'Associated LDAP Groups', bodyHtml: groupsTabHtml},
{id: 'children', label: 'Children', bodyHtml: childrenTabHtml},
{id: 'secrets', label: 'Secrets & OpenBao', bodyHtml: secretsTabHtml},
{id: 'metrics', label: 'Metrics', bodyHtml: metricsTabHtml(resourcesById[id] && resourcesById[id].agent)},
],
footer: {
@@ -527,7 +621,7 @@
},
url: id ? {path: '/directory/' + resourcesById[id].slug} : null,
});
$('#sw-modal-tab-groups-btn, #sw-modal-tab-children-btn').closest('li').toggle(!!id);
$('#sw-modal-tab-groups-btn, #sw-modal-tab-children-btn, #sw-modal-tab-secrets-btn').closest('li').toggle(!!id);
}
function refreshChildrenUI(resourceId) {
@@ -821,8 +915,15 @@
function renderTable() {
const filter = $('#search-filter').val().toLowerCase();
const sort = $('#sort-by').val();
const showPlumbing = $('#toggle-plumbing').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 (!filter) return true;
return (r.name || '').toLowerCase().includes(filter) ||
(r.slug || '').toLowerCase().includes(filter) ||
@@ -1377,8 +1478,188 @@
refreshGroupsUI(r.id);
refreshEdgesUI(r.id);
refreshChildrenUI(r.id);
loadResourceSecrets(r.id);
await loadLdapGroups();
}
var currentResourceSecretsList = [];
var currentParentSecretsList = [];
var rawResourceSecretsMap = {};
const SECRET_KEY_REGEX = /^[A-Za-z0-9_]+$/;
function validateSecretKeyInput(el) {
const $el = $(el);
const val = $el.val().trim();
if (val && !SECRET_KEY_REGEX.test(val)) {
$el.addClass('is-invalid');
return false;
} else {
$el.removeClass('is-invalid');
return true;
}
}
function generateRandomString(len) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?';
const bytes = new Uint8Array(len);
window.crypto.getRandomValues(bytes);
let str = '';
for (let i = 0; i < len; i++) {
str += chars[bytes[i] % chars.length];
}
return str;
}
async function loadResourceSecrets(id) {
const $tbody = $('#secrets-table-body').empty();
$tbody.append('<tr><td colspan="3" class="text-center text-muted py-3"><i class="fa-solid fa-spinner fa-spin me-2"></i>Loading secrets from OpenBao...</td></tr>');
currentResourceSecretsList = [];
currentParentSecretsList = [];
rawResourceSecretsMap = {};
try {
const res = await app.api.get(`directory-admin/resources/${id}/secrets`);
currentResourceSecretsList = (res && res.secrets) || [];
currentParentSecretsList = (res && res.parentSecrets) || [];
renderSecretsTable();
populateParentSecretsDropdown();
} catch (err) {
$tbody.empty().append(`<tr><td colspan="3" class="text-center text-danger py-3"><i class="fa-solid fa-triangle-exclamation me-2"></i>Failed to load secrets: ${esc(err.message || 'Unknown error')}</td></tr>`);
}
}
function populateParentSecretsDropdown() {
const $card = $('#inherit-secret-card');
const $select = $('#inherit-parent-select').empty();
if (!currentParentSecretsList || currentParentSecretsList.length === 0) {
$card.hide();
return;
}
currentParentSecretsList.forEach(p => {
const valStr = `INHERIT:${p.parentSlug}:${p.key}`;
const labelStr = `${p.parentName || p.parentSlug} → ${p.key}`;
$select.append(`<option value="${esc(valStr)}">${esc(labelStr)}</option>`);
});
$card.show();
}
function renderSecretsTable() {
const $tbody = $('#secrets-table-body').empty();
if (currentResourceSecretsList.length === 0) {
$tbody.append('<tr><td colspan="3" class="text-center text-muted py-3">No secrets configured for this resource yet.</td></tr>');
return;
}
currentResourceSecretsList.forEach((s, idx) => {
const $row = $(`
<tr class="secret-row" data-key="${esc(s.key)}">
<td><code class="fw-bold fs-6">${esc(s.key)}</code></td>
<td>
${s.isInherited
? `<span class="badge bg-info text-dark shadow-sm"><i class="fa-solid fa-link me-1"></i>Inherited from ${esc(s.parentSlug || 'Parent')}</span> <small class="text-muted ms-1">(${esc(s.parentKey || s.key)})</small>`
: `<span class="badge bg-success shadow-sm"><i class="fa-solid fa-lock me-1"></i>Configured in OpenBao</span> <span class="badge bg-secondary ms-1"><i class="fa-solid fa-eye-slash me-1"></i>Secret Value Hidden</span>`
}
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-secondary" onclick="editSecretKey('${esc(s.key)}')" title="Set / Overwrite Value"><i class="fa-solid fa-pen me-1"></i> Edit Value</button>
<button class="btn btn-sm btn-outline-danger ms-1" onclick="deleteSecretKey('${esc(s.key)}')" title="Delete Secret"><i class="fa-solid fa-trash"></i></button>
</td>
</tr>
`);
$tbody.append($row);
});
}
function generateSecretValue() {
let key = $('#new-secret-key').val().trim();
if (!key) {
key = 'SECRET_KEY';
$('#new-secret-key').val(key);
}
const len = parseInt($('#gen-secret-length').val(), 10) || 32;
const randomSecret = generateRandomString(len);
$('#new-secret-val').val(randomSecret);
$('#gen-secret-notice').show();
}
function editSecretKey(key) {
$('#new-secret-key').val(key);
$('#new-secret-val').val('').focus();
$('#gen-secret-notice').hide();
}
async function addSecretRow() {
const keyEl = $('#new-secret-key')[0];
const key = $('#new-secret-key').val().trim();
const val = $('#new-secret-val').val();
if (!key) {
app.messages.action('Please enter a secret key name (e.g. DB_PASSWORD).', $('#secrets-tab-container'), 'warning');
return;
}
if (!validateSecretKeyInput(keyEl)) {
app.messages.action('Invalid secret key format. Only uppercase/lowercase letters, numbers, and underscores are allowed (e.g. DB_PASSWORD).', $('#secrets-tab-container'), 'danger');
return;
}
rawResourceSecretsMap[key] = val || '';
$('#new-secret-key').val('');
$('#new-secret-val').val('');
$('#gen-secret-notice').hide();
await saveResourceSecretsMap();
}
async function inheritParentSecret() {
const childKey = $('#inherit-child-key').val().trim();
const inheritVal = $('#inherit-parent-select').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;
}
rawResourceSecretsMap[childKey] = inheritVal;
$('#inherit-child-key').val('');
await saveResourceSecretsMap();
}
async function deleteSecretKey(key) {
const confirmed = await app.messages.confirm(`Delete secret '${key}' from OpenBao?`, $('#secrets-tab-container'), 'danger');
if (!confirmed) return;
delete rawResourceSecretsMap[key];
await saveResourceSecretsMap();
}
async function saveResourceSecretsMap() {
const resourceId = $('#res-id').val();
if (!resourceId) return;
try {
app.messages.action('Saving secrets to OpenBao...', $('#secrets-tab-container'), 'info');
await app.api.post(`directory-admin/resources/${resourceId}/secrets`, { secrets: rawResourceSecretsMap });
app.messages.action('Secret saved to OpenBao successfully!', $('#secrets-tab-container'), 'success');
loadResourceSecrets(resourceId);
} catch (err) {
app.messages.action(err.message || 'Failed to save secrets to OpenBao', $('#secrets-tab-container'), 'danger');
}
}
function refreshResourceSecrets() {
const resourceId = $('#res-id').val();
if (resourceId) loadResourceSecrets(resourceId);
}
async function saveResource() {
// Promote path: the modal was opened from a discovered inventory row, so