Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ba2ace835 | |||
| 25b0d57a97 |
@@ -1303,6 +1303,39 @@ Errors: `400` if the plugin type is unknown, the slug is malformed/duplicated, o
|
|||||||
|
|
||||||
**`DELETE /api/plugins/:id`** — unschedules, removes the OpenBao secret namespace, and deletes the row.
|
**`DELETE /api/plugins/:id`** — unschedules, removes the OpenBao secret namespace, and deletes the row.
|
||||||
|
|
||||||
|
## Configuration Endpoints
|
||||||
|
|
||||||
|
Base path: `/api/conf`
|
||||||
|
|
||||||
|
All endpoints require authentication and `app_sso_admin` membership. Runtime configuration (SMTP, discovery, OAuth) is stored in OpenBao at `secret/sso-manager/conf` and overlaid onto the live app config; changes take effect immediately and persist across restarts. Secret fields (`smtp.pass`, `oauth.jwtSecret`) are **always returned masked** (`********`); submit a blank or `********` value to keep the current stored secret, or a new non-blank value to replace it.
|
||||||
|
|
||||||
|
### Get Configuration
|
||||||
|
|
||||||
|
**`GET /api/conf`** — returns the editable config groups (`smtp`, `discovery`, `oauth`) with secret fields masked to `********`.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"smtp": { "host": "smtp.example.com", "port": 587, "secure": false, "user": "noreply@example.com", "pass": "********", "from": "SSO Manager <noreply@example.com>" },
|
||||||
|
"discovery": { },
|
||||||
|
"oauth": { "issuer": "https://sso.example.com", "jwtSecret": "********", "token_lifetime": { "access_token": 3600, "refresh_token": 2592000 } }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Save Configuration
|
||||||
|
|
||||||
|
**`POST /api/conf`** — deep-merges the submitted groups into `secret/sso-manager/conf` (per-key shallow merge of nested objects) and re-applies them to the live config. A blank or `********` value for `smtp.pass` or `oauth.jwtSecret` preserves the stored secret.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"smtp": { "host": "smtp.example.com", "port": 587, "secure": false, "user": "noreply@example.com", "pass": "********", "from": "SSO Manager <noreply@example.com>" },
|
||||||
|
"oauth": { "issuer": "https://sso.example.com", "token_lifetime": { "access_token": 3600, "refresh_token": 2592000 } }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:** `{ "success": true }`
|
||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
All endpoints return errors in this format:
|
All endpoints return errors in this format:
|
||||||
|
|||||||
@@ -4,6 +4,36 @@ All notable changes to this project are documented here. Format loosely
|
|||||||
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions
|
||||||
correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`.
|
correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`.
|
||||||
|
|
||||||
|
## [1.17.1] - 2026-08-01
|
||||||
|
|
||||||
|
Hardens the **runtime SMTP/OAuth secret handling** on the `/conf` admin page to
|
||||||
|
match the plugin-secrets discipline: the SMTP password and OAuth JWT secret are
|
||||||
|
no longer returned in cleartext by `GET /api/conf` or round-tripped through the
|
||||||
|
form. They remain saved in OpenBao at `secret/sso-manager/conf` at runtime
|
||||||
|
(unchanged) — only how they're surfaced to the admin changes.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **`GET /api/conf`** now masks `smtp.pass` and `oauth.jwtSecret` to `********`
|
||||||
|
(was: returned in cleartext). Non-secret fields (host, port, user, from,
|
||||||
|
secure, issuer, token lifetimes) are returned as before.
|
||||||
|
- **`POST /api/conf`** now treats a blank or `********` secret-field submission
|
||||||
|
as "keep the current stored value" — so an admin editing the From address or
|
||||||
|
token lifetimes no longer has to re-enter (or leak) the SMTP password / JWT
|
||||||
|
secret. Only a genuinely new, non-blank value overwrites. The preserved values
|
||||||
|
are re-applied to live `conf` immediately, as before.
|
||||||
|
- **`/conf` page** (`views/conf.ejs`): the Password and JWT Secret fields carry
|
||||||
|
a "leave unchanged to keep the current value stored in OpenBao" hint; the page
|
||||||
|
copy notes secret fields are masked. No JSON-textarea editing is involved —
|
||||||
|
SMTP is and remains configured through structured form fields.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- SMTP (and OAuth) config was **already** saved to OpenBao at runtime before
|
||||||
|
this release (via `POST /api/conf` → `baoConf.set('sso-manager/conf')`, and
|
||||||
|
overlaid back at boot by `bao-conf.init`). This release closes the
|
||||||
|
cleartext-exposure gap; it does not move the storage path.
|
||||||
|
- No theta-suite policy change required — `secret/sso-manager/conf` was already
|
||||||
|
granted to the `sso-broker` policy.
|
||||||
|
|
||||||
## [1.17.0] - 2026-08-01
|
## [1.17.0] - 2026-08-01
|
||||||
|
|
||||||
A real **plugin system**: the half-built discovery plugins (statically
|
A real **plugin system**: the half-built discovery plugins (statically
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ When the environment is initialized via `setup.sh`, OpenBao is automatically uns
|
|||||||
|
|
||||||
The SSO Manager Vault can be accessed in two ways:
|
The SSO Manager Vault can be accessed in two ways:
|
||||||
|
|
||||||
1. **Via the SSO Manager UI**: Go to the **Admin Configuration** page (`/conf`) to edit the application's configuration secrets directly.
|
1. **Via the SSO Manager UI**: Go to the **Admin Configuration** page (`/conf`) to edit the application's configuration secrets directly. SMTP and OAuth settings are edited through structured form fields (not a raw JSON blob) and saved to OpenBao at `secret/sso-manager/conf` at runtime, taking effect immediately. Secret fields — the SMTP password and the OAuth JWT secret — are returned masked (`********`); leave the field unchanged (or blank) to keep the stored value, or enter a new value to replace it.
|
||||||
2. **Via the REST API**: Send requests to `/api/vault/v1/...` with your SSO Manager session or API Token.
|
2. **Via the REST API**: Send requests to `/api/vault/v1/...` with your SSO Manager session or API Token.
|
||||||
|
|
||||||
### API Example
|
### API Example
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "t42-sso-manager",
|
"name": "t42-sso-manager",
|
||||||
"version": "1.17.0",
|
"version": "1.17.1",
|
||||||
"description": "A very simple LDAP management and SSO system",
|
"description": "A very simple LDAP management and SSO system",
|
||||||
"author": [
|
"author": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,12 +12,31 @@ router.use(async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Secret fields stored inside secret/sso-manager/conf. These are NEVER returned
|
||||||
|
// in cleartext by GET /api/conf (masked to MASK below) and, on save, a blank or
|
||||||
|
// mask-valued submission preserves the stored value so an admin editing an
|
||||||
|
// unrelated field (e.g. the From address) doesn't have to re-enter — or leak —
|
||||||
|
// the SMTP password / OAuth JWT secret. Mirrors the plugin-secrets discipline.
|
||||||
|
const MASK = '********';
|
||||||
|
const SECRET_PATHS = [
|
||||||
|
['smtp', 'pass'],
|
||||||
|
['oauth', 'jwtSecret'],
|
||||||
|
];
|
||||||
|
|
||||||
|
function maskSecrets(obj) {
|
||||||
|
const out = JSON.parse(JSON.stringify(obj));
|
||||||
|
for (const [grp, key] of SECRET_PATHS) {
|
||||||
|
if (out[grp] && out[grp][key]) out[grp][key] = MASK;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
const editable = {
|
const editable = maskSecrets({
|
||||||
smtp: conf.smtp || {},
|
smtp: conf.smtp || {},
|
||||||
discovery: conf.discovery || {},
|
discovery: conf.discovery || {},
|
||||||
oauth: conf.oauth || {}
|
oauth: conf.oauth || {}
|
||||||
};
|
});
|
||||||
res.json(editable);
|
res.json(editable);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,18 +57,31 @@ function applyToLiveConf(src) {
|
|||||||
router.post('/', async (req, res, next) => {
|
router.post('/', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const existing = await baoConf.get('sso-manager/conf') || {};
|
const existing = await baoConf.get('sso-manager/conf') || {};
|
||||||
// Deep merge req.body into existing
|
const incoming = req.body || {};
|
||||||
for (const key of Object.keys(req.body)) {
|
|
||||||
if (typeof req.body[key] === 'object' && req.body[key] !== null && !Array.isArray(req.body[key])) {
|
// Preserve secret fields the admin left blank (or left showing the mask):
|
||||||
existing[key] = { ...(existing[key] || {}), ...req.body[key] };
|
// drop them from the incoming merge so the stored value survives. Only a
|
||||||
|
// genuinely new, non-blank, non-mask value overwrites.
|
||||||
|
for (const [grp, key] of SECRET_PATHS) {
|
||||||
|
if (incoming[grp] && incoming[grp][key] !== undefined) {
|
||||||
|
const submitted = incoming[grp][key];
|
||||||
|
if (submitted === '' || submitted === MASK) delete incoming[grp][key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deep merge incoming into existing
|
||||||
|
for (const key of Object.keys(incoming)) {
|
||||||
|
if (typeof incoming[key] === 'object' && incoming[key] !== null && !Array.isArray(incoming[key])) {
|
||||||
|
existing[key] = { ...(existing[key] || {}), ...incoming[key] };
|
||||||
} else {
|
} else {
|
||||||
existing[key] = req.body[key];
|
existing[key] = incoming[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await baoConf.set('sso-manager/conf', existing);
|
await baoConf.set('sso-manager/conf', existing);
|
||||||
// Reflect the saved values in the live conf immediately (the next boot's
|
// Reflect the saved values in the live conf immediately (the next boot's
|
||||||
// bao-conf.init() would pick them up too, but this keeps running readers
|
// bao-conf.init() would pick them up too, but this keeps running readers
|
||||||
// current without a restart, as the old conf_manager did).
|
// current without a restart, as the old conf_manager did). `existing`
|
||||||
|
// carries the preserved secret values, so live conf keeps them too.
|
||||||
applyToLiveConf(existing);
|
applyToLiveConf(existing);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
|
|||||||
@@ -83,7 +83,9 @@
|
|||||||
<h2><i class="fas fa-cogs"></i> System Configuration</h2>
|
<h2><i class="fas fa-cogs"></i> System Configuration</h2>
|
||||||
<p class="text-muted mb-0">
|
<p class="text-muted mb-0">
|
||||||
Manage runtime configuration such as SMTP settings and OAuth parameters.
|
Manage runtime configuration such as SMTP settings and OAuth parameters.
|
||||||
These secrets are stored securely in OpenBao Vault.
|
These are stored securely in OpenBao and take effect immediately. Secret fields
|
||||||
|
(the SMTP password and OAuth JWT secret) are masked — leave them unchanged to
|
||||||
|
keep the stored value.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -115,9 +117,10 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Password</label>
|
<label class="form-label">Password</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="password" class="form-control" id="smtp-pass">
|
<input type="password" class="form-control" id="smtp-pass" placeholder="********">
|
||||||
<button class="btn btn-outline-secondary" type="button" onclick="togglePassword('smtp-pass')"><i class="fas fa-eye"></i></button>
|
<button class="btn btn-outline-secondary" type="button" onclick="togglePassword('smtp-pass')"><i class="fas fa-eye"></i></button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-text">Leave unchanged to keep the current password stored in OpenBao. Clear and type a new value to replace it.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">From Address</label>
|
<label class="form-label">From Address</label>
|
||||||
@@ -144,9 +147,10 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">JWT Secret</label>
|
<label class="form-label">JWT Secret</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="password" class="form-control" id="oauth-jwtsecret">
|
<input type="password" class="form-control" id="oauth-jwtsecret" placeholder="********">
|
||||||
<button class="btn btn-outline-secondary" type="button" onclick="togglePassword('oauth-jwtsecret')"><i class="fas fa-eye"></i></button>
|
<button class="btn btn-outline-secondary" type="button" onclick="togglePassword('oauth-jwtsecret')"><i class="fas fa-eye"></i></button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-text">Leave unchanged to keep the current secret stored in OpenBao. Clear and type a new value to replace it.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Access Token Lifetime (seconds)</label>
|
<label class="form-label">Access Token Lifetime (seconds)</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user