fix(sso): align conf page design, fix directory inventory filter & plugin modal, fix vault 403 & add shared secrets v1.20.2
Pull Request Tests / Run Tests (18.x) (push) Failing after 56s
Pull Request Tests / Run Tests (20.x) (push) Failing after 28s
Pull Request Tests / Run Tests (22.x) (push) Failing after 28s
Pull Request Tests / Test Summary (push) Failing after 4s

This commit is contained in:
2026-08-03 15:26:59 -04:00
parent 0c5159c49b
commit d8242b1d53
5 changed files with 186 additions and 154 deletions
+2 -2
View File
@@ -29,8 +29,8 @@ describe('vault_broker admin policy', () => {
return { status: 404, text: async () => '' };
}
if (method === 'PUT' && path === 'sys/policies/acl/sso-admin') {
expect(body.policy).toContain('path "secret/metadata" { capabilities = ["list", "read", "delete"] }');
expect(body.policy).toContain('path "secret/metadata/" { capabilities = ["list", "read", "delete"] }');
expect(body.policy).toContain('path "secret/metadata" { capabilities = ["create", "read", "update", "delete", "list"] }');
expect(body.policy).toContain('path "secret/metadata/" { capabilities = ["create", "read", "update", "delete", "list"] }');
return { status: 204, ok: true };
}
if (method === 'POST' && path === 'auth/token/create/sso-broker') {
+31 -28
View File
@@ -79,17 +79,19 @@ async function mintToken(policies) {
return { token, ttl };
}
// ── Per-user token ──────────────────────────────────────────────────────────
// ── Per-user token ──────────────────────────────────────────────────────────
function userPolicyHcl(uid) {
// uid is an LDAP uid (alphanumeric + a few separators); it is interpolated
// into a policy path, so reject anything but a safe charset.
// The bare `secret/metadata/users/<uid>` grant is required to LIST the
// contents of the namespace: `.../*` covers nested paths but NOT the
// directory itself, so without it the /vault secrets list 403s.
return `path "secret/data/users/${uid}/*" { capabilities = ["create", "read", "update", "delete", "list"] }
path "secret/metadata/users/${uid}" { capabilities = ["list", "read", "delete"] }
path "secret/metadata/users/${uid}/" { capabilities = ["list", "read", "delete"] }
path "secret/metadata/users/${uid}/*" { capabilities = ["list", "read", "delete"] }`;
return `path "secret/data/users/${uid}" { capabilities = ["create", "read", "update", "delete", "list"] }
path "secret/data/users/${uid}/*" { capabilities = ["create", "read", "update", "delete", "list"] }
path "secret/metadata/users/${uid}" { capabilities = ["create", "read", "update", "delete", "list"] }
path "secret/metadata/users/${uid}/" { capabilities = ["create", "read", "update", "delete", "list"] }
path "secret/metadata/users/${uid}/*" { capabilities = ["create", "read", "update", "delete", "list"] }
path "secret/data/shared" { capabilities = ["read", "list"] }
path "secret/data/shared/*" { capabilities = ["read", "list"] }
path "secret/metadata/shared" { capabilities = ["read", "list"] }
path "secret/metadata/shared/" { capabilities = ["read", "list"] }
path "secret/metadata/shared/*" { capabilities = ["read", "list"] }`;
}
// Mint (or return the cached) per-user token confined to secret/users/<uid>/*.
@@ -107,13 +109,13 @@ async function getOrCreateUserToken(uid) {
// ── Admin token (read/write all of secret/) ─────────────────────────────────
function adminPolicyHcl() {
// The bare `secret/metadata` / `secret/metadata/` grants let an admin LIST
// the KV mount root (the top-level dirs); `secret/metadata/*` covers nested
// paths but NOT the root itself, so without it the /vault secrets list 403s.
return `path "secret/data/*" { capabilities = ["create", "read", "update", "delete", "list"] }
path "secret/metadata" { capabilities = ["list", "read", "delete"] }
path "secret/metadata/" { capabilities = ["list", "read", "delete"] }
path "secret/metadata/*" { capabilities = ["list", "read", "delete"] }`;
return `path "secret/*" { capabilities = ["create", "read", "update", "delete", "list"] }
path "secret" { capabilities = ["create", "read", "update", "delete", "list"] }
path "secret/data/*" { capabilities = ["create", "read", "update", "delete", "list"] }
path "secret/data" { capabilities = ["create", "read", "update", "delete", "list"] }
path "secret/metadata" { capabilities = ["create", "read", "update", "delete", "list"] }
path "secret/metadata/" { capabilities = ["create", "read", "update", "delete", "list"] }
path "secret/metadata/*" { capabilities = ["create", "read", "update", "delete", "list"] }`;
}
async function getOrCreateAdminToken(uid) {
@@ -128,11 +130,16 @@ async function getOrCreateAdminToken(uid) {
// ── Per-app token (minted ONCE, returned to the caller, never cached) ───────
function appPolicyHcl(name) {
// The bare `secret/metadata/apps/<name>` grant lets an app LIST its own
// namespace root (see userPolicyHcl for why `/*` alone isn't enough).
return `path "secret/data/apps/${name}/*" { capabilities = ["create", "read", "update", "delete", "list"] }
path "secret/metadata/apps/${name}" { capabilities = ["list", "read", "delete"] }
path "secret/metadata/apps/${name}/*" { capabilities = ["list", "read", "delete"] }`;
return `path "secret/data/apps/${name}" { capabilities = ["create", "read", "update", "delete", "list"] }
path "secret/data/apps/${name}/*" { capabilities = ["create", "read", "update", "delete", "list"] }
path "secret/metadata/apps/${name}" { capabilities = ["create", "read", "update", "delete", "list"] }
path "secret/metadata/apps/${name}/" { capabilities = ["create", "read", "update", "delete", "list"] }
path "secret/metadata/apps/${name}/*" { capabilities = ["create", "read", "update", "delete", "list"] }
path "secret/data/shared" { capabilities = ["read", "list"] }
path "secret/data/shared/*" { capabilities = ["read", "list"] }
path "secret/metadata/shared" { capabilities = ["read", "list"] }
path "secret/metadata/shared/" { capabilities = ["read", "list"] }
path "secret/metadata/shared/*" { capabilities = ["read", "list"] }`;
}
// Create the app-<name> policy + mint a token for it. Returns the token ONCE
@@ -190,17 +197,13 @@ async function scopeGuard(req, res, next) {
return res.status(503).json({ error: 'vault broker unavailable', detail: e.message });
}
// Defense-in-depth: confirm the requested path is within the subject's
// namespace. Admins roam all of secret/; users are confined to
// secret/users/<uid>/. (The token's own policy enforces the same at the
// OpenBao layer; this catches a buggy/malicious client early with a clear
// 403 instead of an opaque OpenBao denial.)
const norm = normalizeVaultPath(req.path);
if (norm === null) {
return res.status(403).json({ error: 'vault paths must be under /secret/' });
}
const base = `/secret/users/${uid}`;
const allowed = admin || norm === base || norm.startsWith(base + '/');
const userBase = `/secret/users/${uid}`;
const sharedBase = `/secret/shared`;
const allowed = admin || norm === userBase || norm.startsWith(userBase + '/') || norm === sharedBase || norm.startsWith(sharedBase + '/');
if (!allowed) {
return res.status(403).json({ error: 'path outside your vault namespace' });
}
+68 -111
View File
@@ -1,4 +1,5 @@
<%- include('top') %>
<script type="text/javascript">
app.auth.forceLogin(['admin', 'app_sso_admin']);
@@ -48,7 +49,7 @@
async function saveConf() {
const btn = $('#btn-save');
btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Saving...');
btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-1"></i> Saving...');
const payload = {
smtp: {
@@ -76,7 +77,7 @@
try {
await app.api.post('conf', payload);
app.messages.toast('Configuration saved successfully! It will take effect immediately.', 'success');
app.messages.toast('Configuration saved successfully!', 'success');
} catch (error) {
app.messages.toast('Failed to save configuration: ' + error.message, 'danger');
} finally {
@@ -93,7 +94,7 @@
const btn = $('#btn-test-email');
const originalHtml = btn.html();
btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Sending...');
btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-1"></i> Sending...');
try {
const payload = {
@@ -126,7 +127,7 @@
const btn = $('#btn-test-sms');
const originalHtml = btn.html();
btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Sending...');
btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-1"></i> Sending...');
try {
const payload = {
@@ -174,7 +175,7 @@
async function saveProxyConf() {
const btn = $('#btn-save-proxy');
btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Saving...');
btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-1"></i> Saving...');
const payload = {
oidc: {
@@ -254,7 +255,7 @@
function renderMessagingPlugins() {
const $list = $('#messaging-plugins-list').empty();
if (messagingPlugins.length === 0) {
$list.append('<div class="text-muted text-center py-3"><i class="fas fa-plug text-secondary mb-2 fs-3"></i><br>No messaging plugins configured.</div>');
$list.append('<div class="text-muted text-center py-4"><i class="fas fa-plug text-black-50 fs-2 mb-2"></i><br>No messaging plugins configured.</div>');
return;
}
messagingPlugins.forEach(p => {
@@ -303,76 +304,52 @@
}
</script>
<div class="container-fluid py-4 px-md-4">
<!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4 pb-3 border-bottom">
<div>
<h3 class="fw-bold mb-1"><i class="fas fa-sliders-h text-primary me-2"></i> System Configuration</h3>
<p class="text-muted mb-0 small">
Manage stack configuration (SMTP, OAuth, VoIP.ms, Proxy, Terms of Service, and Messaging Plugins). Secrets are stored in OpenBao.
</p>
</div>
<div>
<button class="btn btn-outline-secondary me-2" onclick="loadConf()"><i class="fas fa-rotate me-1"></i> Reset</button>
<button id="btn-save" class="btn btn-primary" onclick="saveConf()"><i class="fas fa-save me-1"></i> Save Configuration</button>
</div>
</div>
<div class="row g-4">
<!-- Sidebar Navigation -->
<div class="col-lg-3 col-md-4">
<div class="card shadow-sm border-0 sticky-top" style="top: var(--sw-content-offset, 70px);">
<div class="list-group list-group-flush rounded-3" id="conf-nav-list" role="tablist">
<a class="list-group-item list-group-item-action active d-flex align-items-center py-3" id="tab-nav-oauth" data-bs-toggle="list" href="#pane-oauth" role="tab">
<i class="fas fa-key text-success me-3 fs-5" style="width: 24px;"></i>
<div>
<div class="fw-semibold">OAuth & JWT</div>
<div class="small text-muted">Issuer & Token Lifetimes</div>
</div>
</a>
<a class="list-group-item list-group-item-action d-flex align-items-center py-3" id="tab-nav-smtp" data-bs-toggle="list" href="#pane-smtp" role="tab">
<i class="fas fa-envelope text-primary me-3 fs-5" style="width: 24px;"></i>
<div>
<div class="fw-semibold">Email (SMTP)</div>
<div class="small text-muted">Mail Delivery & Testing</div>
</div>
</a>
<a class="list-group-item list-group-item-action d-flex align-items-center py-3" id="tab-nav-sms" data-bs-toggle="list" href="#pane-sms" role="tab">
<i class="fas fa-comment-sms text-info me-3 fs-5" style="width: 24px;"></i>
<div>
<div class="fw-semibold">SMS & Messaging</div>
<div class="small text-muted">VoIP.ms & Webhook Plugins</div>
</div>
</a>
<a class="list-group-item list-group-item-action d-flex align-items-center py-3" id="tab-nav-proxy" data-bs-toggle="list" href="#pane-proxy" role="tab">
<i class="fas fa-shield-alt text-warning me-3 fs-5" style="width: 24px;"></i>
<div>
<div class="fw-semibold">Proxy Secrets</div>
<div class="small text-muted">OpenBao Integration</div>
</div>
</a>
<a class="list-group-item list-group-item-action d-flex align-items-center py-3" id="tab-nav-tos" data-bs-toggle="list" href="#pane-tos" role="tab">
<i class="fas fa-file-contract text-secondary me-3 fs-5" style="width: 24px;"></i>
<div>
<div class="fw-semibold">Terms of Service</div>
<div class="small text-muted">User Agreement & Policy</div>
</div>
</a>
<div class="container mt-4">
<div class="row">
<div class="col-12">
<div class="card shadow">
<!-- Header with Sub-Nav Tabs matching directory.ejs -->
<div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2">
<ul class="nav nav-tabs card-header-tabs" id="confTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="oauth-tab" data-bs-toggle="tab" data-bs-target="#pane-oauth" type="button" role="tab">
<i class="fas fa-key text-success me-1"></i> OAuth & JWT
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="smtp-tab" data-bs-toggle="tab" data-bs-target="#pane-smtp" type="button" role="tab">
<i class="fas fa-envelope text-primary me-1"></i> Email (SMTP)
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="sms-tab" data-bs-toggle="tab" data-bs-target="#pane-sms" type="button" role="tab">
<i class="fas fa-comment-sms text-info me-1"></i> SMS & Messaging
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="proxy-tab" data-bs-toggle="tab" data-bs-target="#pane-proxy" type="button" role="tab">
<i class="fas fa-shield-alt text-warning me-1"></i> Proxy Secrets
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tos-tab" data-bs-toggle="tab" data-bs-target="#pane-tos" type="button" role="tab">
<i class="fas fa-file-contract text-secondary me-1"></i> Terms of Service
</button>
</li>
</ul>
<div>
<button class="btn btn-sm btn-outline-secondary me-1" onclick="loadConf()"><i class="fas fa-rotate me-1"></i> Reset</button>
<button id="btn-save" class="btn btn-sm btn-primary" onclick="saveConf()"><i class="fas fa-save me-1"></i> Save Configuration</button>
</div>
</div>
</div>
</div>
<!-- Main Content Panes -->
<div class="col-lg-9 col-md-8">
<div class="tab-content" id="conf-tab-content">
<div class="card-body p-4">
<div class="tab-content" id="confTabContent">
<!-- OAuth Pane -->
<div class="tab-pane fade show active" id="pane-oauth" role="tabpanel">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white pt-4 pb-3 border-bottom">
<h5 class="mb-0 fw-bold"><i class="fas fa-key text-success me-2"></i> OAuth 2.0 & JWT Settings</h5>
</div>
<div class="card-body p-4">
<!-- OAuth & JWT Tab -->
<div class="tab-pane fade show active" id="pane-oauth" role="tabpanel">
<h5 class="fw-bold mb-3"><i class="fas fa-key text-success me-2"></i> OAuth 2.0 & JWT Settings</h5>
<p class="text-muted small">Configure OIDC issuer URLs, token lifetimes, and JWT signing keys. Stored in OpenBao.</p>
<div class="mb-3">
<label class="form-label fw-semibold">Issuer URL</label>
<input type="text" class="form-control" id="oauth-issuer" placeholder="https://sso.example.com">
@@ -396,16 +373,11 @@
</div>
</div>
</div>
</div>
</div>
<!-- SMTP Pane -->
<div class="tab-pane fade" id="pane-smtp" role="tabpanel">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white pt-4 pb-3 border-bottom">
<h5 class="mb-0 fw-bold"><i class="fas fa-envelope text-primary me-2"></i> SMTP Server Settings</h5>
</div>
<div class="card-body p-4">
<!-- SMTP Tab -->
<div class="tab-pane fade" id="pane-smtp" role="tabpanel">
<h5 class="fw-bold mb-3"><i class="fas fa-envelope text-primary me-2"></i> SMTP Server Settings</h5>
<p class="text-muted small">System mail server credentials for password resets, notifications, and verification emails.</p>
<div class="row">
<div class="col-md-8 mb-3">
<label class="form-label fw-semibold">SMTP Host</label>
@@ -449,16 +421,11 @@
<div class="form-text">Saves current SMTP config and sends a test message.</div>
</div>
</div>
</div>
</div>
<!-- SMS & Messaging Pane -->
<div class="tab-pane fade" id="pane-sms" role="tabpanel">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white pt-4 pb-3 border-bottom">
<h5 class="mb-0 fw-bold"><i class="fas fa-comment-sms text-info me-2"></i> VoIP.ms SMS Integration</h5>
</div>
<div class="card-body p-4">
<!-- SMS & Messaging Tab -->
<div class="tab-pane fade" id="pane-sms" role="tabpanel">
<h5 class="fw-bold mb-3"><i class="fas fa-comment-sms text-info me-2"></i> VoIP.ms SMS Integration</h5>
<p class="text-muted small">Configure VoIP.ms API credentials for delivering SMS 2FA codes.</p>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-semibold">API Username</label>
@@ -495,16 +462,10 @@
</div>
<div id="messaging-plugins-list"></div>
</div>
</div>
</div>
<!-- Proxy Secrets Pane -->
<div class="tab-pane fade" id="pane-proxy" role="tabpanel">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white pt-4 pb-3 border-bottom">
<h5 class="mb-0 fw-bold"><i class="fas fa-shield-alt text-warning me-2"></i> OpenBao Proxy Integration</h5>
</div>
<div class="card-body p-4">
<!-- Proxy Secrets Tab -->
<div class="tab-pane fade" id="pane-proxy" role="tabpanel">
<h5 class="fw-bold mb-3"><i class="fas fa-shield-alt text-warning me-2"></i> OpenBao Proxy Integration</h5>
<p class="text-muted small">Secrets stored directly in OpenBao (<code>secret/proxy/conf</code>) and consumed by Proxy at boot.</p>
<h6 class="fw-bold text-dark mt-3 mb-2">OAuth / OIDC Client</h6>
@@ -537,17 +498,13 @@
<button id="btn-save-proxy" class="btn btn-warning mt-2 text-dark fw-semibold" onclick="saveProxyConf()"><i class="fas fa-save me-1"></i> Save Proxy Secrets</button>
</div>
</div>
</div>
<!-- Terms of Service Pane -->
<div class="tab-pane fade" id="pane-tos" role="tabpanel">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white pt-4 pb-3 border-bottom d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-bold"><i class="fas fa-file-contract me-2"></i> Terms of Service Editor</h5>
<span class="small text-muted" id="tos-meta"></span>
</div>
<div class="card-body p-4">
<!-- Terms of Service Tab -->
<div class="tab-pane fade" id="pane-tos" role="tabpanel">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="fw-bold mb-0"><i class="fas fa-file-contract me-2"></i> Terms of Service Editor</h5>
<span class="small text-muted" id="tos-meta"></span>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Terms Content (Markdown)</label>
<textarea class="form-control font-monospace" id="tos-content" rows="10" placeholder="Enter Terms of Service markdown content..."></textarea>
@@ -559,9 +516,9 @@
<button class="btn btn-primary" onclick="saveTos()"><i class="fas fa-floppy-disk me-1"></i> Save Terms of Service</button>
<div id="tos-result" style="display:none" class="mt-3"></div>
</div>
</div>
</div>
</div>
</div>
</div>
+76 -10
View File
@@ -201,7 +201,10 @@
<h5 class="fw-bold mb-1"><i class="fa-solid fa-plug text-primary me-2"></i> Discovery Plugins</h5>
<p class="text-muted small mb-0">Manage background discovery agents (Nmap, Docker, Proxmox, UniFi). Per-instance secrets are stored in OpenBao.</p>
</div>
<button class="btn btn-sm btn-outline-primary" onclick="loadDiscoveryPlugins()"><i class="fas fa-rotate me-1"></i> Refresh</button>
<div>
<button class="btn btn-sm btn-outline-primary me-2" onclick="loadDiscoveryPlugins()"><i class="fas fa-rotate me-1"></i> Refresh</button>
<button class="btn btn-sm btn-primary shadow-sm" onclick="openNewDiscoveryPluginModal()"><i class="fas fa-plus me-1"></i> New Plugin</button>
</div>
</div>
<div id="discovery-plugins-list" class="mt-3"></div>
</div>
@@ -1259,9 +1262,10 @@
function renderDiscoveryTable() {
const search = $('#discovery-search-filter').val().toLowerCase();
const filtered = allDiscoveryResources.filter(r => {
if(search && !r.name.toLowerCase().includes(search) && !r.slug.toLowerCase().includes(search)) return false;
const isManaged = !!(r.metadata && r.metadata.managed);
if(isManaged) return false;
if (search && !r.name.toLowerCase().includes(search) && !r.slug.toLowerCase().includes(search)) return false;
// Directory contains managed items; Discovered Inventory only shows unmanaged/pending items awaiting promotion
const isExplicitManaged = r.metadata && (r.metadata.managed === true || r.metadata.managed === 'true');
if (isExplicitManaged || r.kind === 'site' || r.kind === 'service') return false;
return true;
});
@@ -1582,15 +1586,77 @@
}
}
async function deleteDiscoveryPlugin(id) {
const ok = await app.messages.confirm('Are you sure you want to delete this discovery plugin?');
if (!ok) return;
var discoveryPluginTypes = [];
function openNewDiscoveryPluginModal() {
app.api.get('plugins/types', function(err, res) {
if (err) { app.messages.toast('Error loading plugin types: ' + err.message, 'danger'); return; }
discoveryPluginTypes = (res.results || []).filter(t => t.category === 'discovery');
if (discoveryPluginTypes.length === 0) {
app.messages.toast('No discovery plugin types available', 'warning');
return;
}
const options = discoveryPluginTypes.map(t => `<option value="${t.type}">${t.name} (${t.type})</option>`).join('');
const bodyHtml = `
<div class="mb-3">
<label class="form-label fw-bold">Plugin Type</label>
<select id="new-plugin-type" class="form-select shadow-sm">${options}</select>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Instance Name</label>
<input type="text" id="new-plugin-name" class="form-control shadow-sm" placeholder="e.g. Local Subnet Scanner">
</div>
<div class="mb-3">
<label class="form-label fw-bold">Slug</label>
<input type="text" id="new-plugin-slug" class="form-control shadow-sm font-monospace" placeholder="e.g. local-subnet-scanner">
</div>
<div class="mb-3">
<label class="form-label fw-bold">Cron Schedule</label>
<input type="text" id="new-plugin-cron" class="form-control shadow-sm font-monospace" value="*/15 * * * *">
<div class="form-text">Standard 5-field cron expression (e.g. */15 * * * * for every 15 mins)</div>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="new-plugin-enabled" checked>
<label class="form-check-label fw-semibold" for="new-plugin-enabled">Enable (load on create)</label>
</div>
<div class="d-flex justify-content-end gap-2">
<button class="btn btn-secondary" onclick="app.modal.close()">Cancel</button>
<button class="btn btn-primary" onclick="saveNewDiscoveryPlugin()">Create Plugin</button>
</div>
`;
app.modal.open({
title: 'Configure New Discovery Plugin',
bodyHtml: bodyHtml,
size: 'md'
});
});
}
async function saveNewDiscoveryPlugin() {
const type = $('#new-plugin-type').val();
const name = $('#new-plugin-name').val().trim();
const slug = $('#new-plugin-slug').val().trim() || name.toLowerCase().replace(/[^a-z0-9]/g, '-');
const cron = $('#new-plugin-cron').val().trim() || '*/15 * * * *';
const enabled = $('#new-plugin-enabled').is(':checked');
if (!name) return app.messages.action('Name is required', app.modal.body(), 'danger');
try {
await app.api.delete(`plugins/${id}`);
app.messages.toast('Discovery plugin deleted', 'success');
await app.api.post('plugins', {
pluginType: type,
name,
slug,
cron,
enabled,
config: {}
});
app.messages.toast('Discovery plugin created successfully!', 'success');
app.modal.close();
loadDiscoveryPlugins();
} catch (e) {
app.messages.toast('Error deleting plugin: ' + e.message, 'danger');
app.messages.action('Error creating plugin: ' + e.message, app.modal.body(), 'danger');
}
}
+8 -2
View File
@@ -134,7 +134,12 @@ curl "$VAULT_ADDR/v1/secret/data/apps/<span id="app-name-display"></span>/conf"
// key relative to the subject's namespace (so 'foo' for a user means
// secret/data/users/<uid>/foo).
function vpath(kind, key) {
return `secret/${kind}/${VAULT_BASE}${key}`;
let cleanKey = key || '';
if (cleanKey.startsWith('/')) cleanKey = cleanKey.slice(1);
if (VAULT_BASE) {
return `secret/${kind}/${VAULT_BASE}${cleanKey}`;
}
return `secret/${kind}/${cleanKey}`;
}
function apiCall(method, path, body = null) {
@@ -156,7 +161,8 @@ curl "$VAULT_ADDR/v1/secret/data/apps/<span id="app-name-display"></span>/conf"
async function loadSecrets() {
try {
const res = await apiCall('GET', vpath('metadata', '?list=true'));
const listPath = vpath('metadata', '').replace(/\/$/, '') + '?list=true';
const res = await apiCall('GET', listPath);
const listEl = document.getElementById('secrets-list');
listEl.innerHTML = '';
if (!res || !res.data || !res.data.keys || res.data.keys.length === 0) {