Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21a56dce50 |
@@ -4,6 +4,24 @@ 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.16.1] - 2026-08-01
|
||||||
|
|
||||||
|
Fix: the Configuration (`/conf`) and Vault (`/vault`) pages returned **401** for
|
||||||
|
a logged-in admin. Both view routes did server-side auth using `req.user`, but
|
||||||
|
this app's auth-token is a header set by client-side JS (localStorage), not a
|
||||||
|
cookie — so `req.user` is undefined on a plain browser navigation.
|
||||||
|
`permission.byGroup(undefined, …)` throws status 401, and the `middleware.auth`
|
||||||
|
gate on `/vault` threw `Auth.errors.login()` (401) for the same reason.
|
||||||
|
|
||||||
|
Both routes now render the shell unconditionally (like `/users`, `/directory`,
|
||||||
|
`/overview`) and gate client-side: `conf.ejs` already called
|
||||||
|
`app.auth.forceLogin(['admin','app_sso_admin'])`; `vault.ejs` now derives
|
||||||
|
`isAdmin` + the personal namespace from `/api/user/me` after `forceLogin()`
|
||||||
|
instead of server-rendering them. The `/api/conf` and `/api/vault` endpoints
|
||||||
|
still enforce `app_sso_admin` + the OpenBao scope server-side, so protection is
|
||||||
|
unchanged — only the view-route gating moved client-side where the session
|
||||||
|
actually lives. Also removed a dead duplicate `/conf` route definition.
|
||||||
|
|
||||||
## [1.16.0] - 2026-08-01
|
## [1.16.0] - 2026-08-01
|
||||||
|
|
||||||
OpenBao becomes the central secrets store for the theta42 stack, and the SSO
|
OpenBao becomes the central secrets store for the theta42 stack, and the SSO
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "t42-sso-manager",
|
"name": "t42-sso-manager",
|
||||||
"version": "1.16.0",
|
"version": "1.16.1",
|
||||||
"description": "A very simple LDAP management and SSO system",
|
"description": "A very simple LDAP management and SSO system",
|
||||||
"author": [
|
"author": [
|
||||||
{
|
{
|
||||||
|
|||||||
+17
-24
@@ -11,8 +11,6 @@ const {Tos} = require('../models/tos');
|
|||||||
const conf = require('@simpleworkjs/conf');
|
const conf = require('@simpleworkjs/conf');
|
||||||
const buildInfo = require('../utils/build_info');
|
const buildInfo = require('../utils/build_info');
|
||||||
const { mountStaticModules } = require('@simpleworkjs/app-stack');
|
const { mountStaticModules } = require('@simpleworkjs/app-stack');
|
||||||
const middleware = require('../middleware/auth');
|
|
||||||
const permission = require('../utils/permission');
|
|
||||||
|
|
||||||
const values ={
|
const values ={
|
||||||
title: conf.environment !== 'production' ? `dev` : '',
|
title: conf.environment !== 'production' ? `dev` : '',
|
||||||
@@ -66,13 +64,15 @@ router.get('/notifications', (req, res) => res.redirect(301, '/overview'));
|
|||||||
router.get('/dashboard', (req, res) => res.redirect(301, '/overview'));
|
router.get('/dashboard', (req, res) => res.redirect(301, '/overview'));
|
||||||
router.get('/executive', (req, res) => res.redirect(301, '/overview'));
|
router.get('/executive', (req, res) => res.redirect(301, '/overview'));
|
||||||
|
|
||||||
router.get('/conf', async function(req, res, next) {
|
router.get('/conf', function(req, res) {
|
||||||
try {
|
// Admin-only Configuration page. The view renders the shell for anyone
|
||||||
await permission.byGroup(req.user, ['app_sso_admin']);
|
// (like /users, /directory, etc.); the client gates access with
|
||||||
res.render('conf', {...values});
|
// app.auth.forceLogin(['admin','app_sso_admin']) and the /api/conf endpoint
|
||||||
} catch(err) {
|
// enforces app_sso_admin server-side. The previous server-side
|
||||||
next(err);
|
// permission.byGroup(req.user,…) 401'd on a browser navigation because this
|
||||||
}
|
// app's auth-token is a header set by client JS (localStorage), not a
|
||||||
|
// cookie — so req.user is undefined on a plain page load.
|
||||||
|
res.render('conf', {...values});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/directory', function(req, res) {
|
router.get('/directory', function(req, res) {
|
||||||
@@ -87,21 +87,18 @@ router.get('/plugins', function(req, res, next) {
|
|||||||
res.redirect('/directory');
|
res.redirect('/directory');
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/vault', middleware.auth, async function(req, res, next) {
|
router.get('/vault', function(req, res) {
|
||||||
// Personal per-user secrets (secret/users/<uid>/*) for everyone; admins get
|
// Personal per-user secrets (secret/users/<uid>/*) for everyone; admins get
|
||||||
// free-form access across all of secret/ plus an Apps tab to mint scoped
|
// free-form access across all of secret/ plus an Apps tab to mint scoped
|
||||||
// tokens for external apps. The /api/vault proxy enforces the same scoping
|
// tokens for external apps. The view renders the shell for any logged-in
|
||||||
// server-side (scopeGuard + the token's own OpenBao policy).
|
// user; the client gates login via app.auth.forceLogin() and derives the
|
||||||
let isAdmin = false;
|
// admin/namespace scope from /api/user/me. The /api/vault proxy enforces the
|
||||||
try {
|
// same scoping server-side (scopeGuard + the token's own OpenBao policy), so
|
||||||
await permission.byGroup(req.user, ['app_sso_admin']);
|
// the client-derived scope is only cosmetic. vaultAddr is the only
|
||||||
isAdmin = true;
|
// server-rendered value (it's a non-user-specific env var); uid + isAdmin
|
||||||
} catch (e) { /* non-admin: personal namespace only */ }
|
// are resolved client-side to avoid the header-vs-navigation auth mismatch.
|
||||||
res.render('vault', {
|
res.render('vault', {
|
||||||
...values,
|
...values,
|
||||||
vaultUid: req.user.uid,
|
|
||||||
vaultIsAdmin: isAdmin,
|
|
||||||
vaultBase: isAdmin ? '' : `users/${req.user.uid}/`,
|
|
||||||
vaultAddr: process.env.VAULT_ADDR || 'http://openbao:8200',
|
vaultAddr: process.env.VAULT_ADDR || 'http://openbao:8200',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -137,10 +134,6 @@ router.get('/users', async function(req, res, next) {
|
|||||||
res.render('users', {...values});
|
res.render('users', {...values});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/conf', async function(req, res, next) {
|
|
||||||
res.render('conf', {...values});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/login', async function(req, res, next) {
|
router.get('/login', async function(req, res, next) {
|
||||||
res.render('login', {...values, redirect: req.query.redirect});
|
res.render('login', {...values, redirect: req.query.redirect});
|
||||||
});
|
});
|
||||||
|
|||||||
+28
-23
@@ -2,15 +2,10 @@
|
|||||||
|
|
||||||
<div class="container-fluid py-4">
|
<div class="container-fluid py-4">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<h2><i class="fas fa-lock"></i>
|
<h2 id="vault-title"><i class="fas fa-lock"></i> My Secrets <small class="text-muted">(personal namespace)</small></h2>
|
||||||
<% if (vaultIsAdmin) { %> Vault Secrets <small class="text-muted">(admin — all of secret/)</small>
|
|
||||||
<% } else { %> My Secrets <small class="text-muted">(personal namespace)</small><% } %>
|
|
||||||
</h2>
|
|
||||||
<ul class="nav nav-pills" id="vault-tabs">
|
<ul class="nav nav-pills" id="vault-tabs">
|
||||||
<li class="nav-item"><button class="nav-link active" data-bs-toggle="pill" data-bs-target="#tab-secrets" type="button">Secrets</button></li>
|
<li class="nav-item"><button class="nav-link active" data-bs-toggle="pill" data-bs-target="#tab-secrets" type="button">Secrets</button></li>
|
||||||
<% if (vaultIsAdmin) { %>
|
<li class="nav-item" id="vault-apps-tab" style="display:none"><button class="nav-link" data-bs-toggle="pill" data-bs-target="#tab-apps" type="button">Apps</button></li>
|
||||||
<li class="nav-item"><button class="nav-link" data-bs-toggle="pill" data-bs-target="#tab-apps" type="button">Apps</button></li>
|
|
||||||
<% } %>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -52,8 +47,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Apps tab (admin only) ───────────────────────────────────────── -->
|
<!-- ── Apps tab (admin only; revealed client-side for admins) ─────── -->
|
||||||
<% if (vaultIsAdmin) { %>
|
|
||||||
<div class="tab-pane fade" id="tab-apps">
|
<div class="tab-pane fade" id="tab-apps">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
@@ -89,7 +83,6 @@ curl "$VAULT_ADDR/v1/secret/data/apps/<span id="app-name-display"></span>/conf"
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -103,10 +96,8 @@ curl "$VAULT_ADDR/v1/secret/data/apps/<span id="app-name-display"></span>/conf"
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">
|
<label class="form-label" id="secret-path-label">Secret name (in your personal namespace)</label>
|
||||||
<% if (vaultIsAdmin) { %>Secret path (under secret/)<% } else { %>Secret name (in your personal namespace)<% } %>
|
<input type="text" class="form-control" id="secret-path-input" placeholder="e.g. database-creds">
|
||||||
</label>
|
|
||||||
<input type="text" class="form-control" id="secret-path-input" placeholder="<% if (vaultIsAdmin) { %>e.g. apps/my-service/conf<% } else { %>e.g. database-creds<% } %>">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Secret Data (JSON)</label>
|
<label class="form-label">Secret Data (JSON)</label>
|
||||||
@@ -126,14 +117,15 @@ curl "$VAULT_ADDR/v1/secret/data/apps/<span id="app-name-display"></span>/conf"
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
app.auth.forceLogin();
|
// Login gate + client-derived scoping. VAULT_BASE is '' for admins
|
||||||
|
// (free-form under secret/) or 'users/<uid>/' for everyone else (confined
|
||||||
// Server-derived scoping. VAULT_BASE is '' for admins (free-form under
|
// to their personal namespace). The /api/vault proxy enforces the same
|
||||||
// secret/) or 'users/<uid>/' for everyone else (confined to their personal
|
// server-side (scopeGuard + the token's OpenBao policy), so this only
|
||||||
// namespace). The /api/vault proxy enforces the same server-side; these only
|
// drives the UI. Resolved in init() after forceLogin loads the user — the
|
||||||
// drive the UI.
|
// previous version read these server-side from req.user, which is undefined
|
||||||
const VAULT_BASE = <%- JSON.stringify(vaultBase) %>;
|
// on a browser navigation (auth-token is a client-set header, not a cookie).
|
||||||
const IS_ADMIN = <%- JSON.stringify(vaultIsAdmin) %>;
|
let VAULT_BASE = '';
|
||||||
|
let IS_ADMIN = false;
|
||||||
|
|
||||||
let currentSecretPath = null;
|
let currentSecretPath = null;
|
||||||
const secretModal = new bootstrap.Modal(document.getElementById('secretModal'));
|
const secretModal = new bootstrap.Modal(document.getElementById('secretModal'));
|
||||||
@@ -309,7 +301,20 @@ curl "$VAULT_ADDR/v1/secret/data/apps/<span id="app-name-display"></span>/conf"
|
|||||||
navigator.clipboard.writeText(text).then(() => app.messages.toast('Copied', 'success'));
|
navigator.clipboard.writeText(text).then(() => app.messages.toast('Copied', 'success'));
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSecrets();
|
(async function init() {
|
||||||
|
const user = await app.auth.forceLogin();
|
||||||
|
if (!user) return; // not logged in — forceLogin redirected to /login
|
||||||
|
IS_ADMIN = app.auth.isAdmin();
|
||||||
|
VAULT_BASE = IS_ADMIN ? '' : 'users/' + user.uid + '/';
|
||||||
|
if (IS_ADMIN) {
|
||||||
|
document.getElementById('vault-apps-tab').style.display = '';
|
||||||
|
document.getElementById('vault-title').innerHTML =
|
||||||
|
'<i class="fas fa-lock"></i> Vault Secrets <small class="text-muted">(admin — all of secret/)</small>';
|
||||||
|
document.getElementById('secret-path-label').textContent = 'Secret path (under secret/)';
|
||||||
|
document.getElementById('secret-path-input').placeholder = 'e.g. apps/my-service/conf';
|
||||||
|
}
|
||||||
|
loadSecrets();
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<%- include('bottom') %>
|
<%- include('bottom') %>
|
||||||
Reference in New Issue
Block a user