diff --git a/CHANGELOG.md b/CHANGELOG.md index 074a5a0..8d4b011 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## v1.35.0 +- feat: **permission entries can be edited.** The Permissions page only offered Delete, so changing a role or scope meant removing the grant and re-adding it from memory. New `PUT /api/permission/:id` plus an Edit modal pre-filled from the record. +- fix: a permission's id is derived from (subjectType, subject, scope, domain), so changing any of those is a *different* record, not an update. The endpoint creates the new grant and removes the superseded one in that order, so an edit can never leave the old grant behind still conferring access. + ## v1.34.0 - feat: the per-host SSO **Allowed groups** field now autocompletes from the SSO directory's groups. Suggestions previously came only from local groups, permission subjects and `conf.auth` maps — none of which can match an SSO-gated host, because its allow-list is checked against the `groups` claim the SSO issues. New `conf.sso` block (`url` + read-only `apiToken`, minted by theta-suite's bootstrap); results are cached for 5 minutes and the endpoint degrades silently to the old local-only list when unset. - fix: the SSO group lookup authenticates with `Authorization: Bearer `, not the `auth-token` header — the latter is for browser session UUIDs and would be rejected for a minted API token. diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 4204e64..8ec7598 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -1,12 +1,12 @@ { "name": "proxy-api", - "version": "1.34.0", + "version": "1.35.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "proxy-api", - "version": "1.34.0", + "version": "1.35.0", "license": "MIT", "dependencies": { "@fortawesome/fontawesome-free": "^7.3.0", diff --git a/nodejs/package.json b/nodejs/package.json index b01c07b..d4a92ca 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -1,6 +1,6 @@ { "name": "proxy-api", - "version": "1.34.0", + "version": "1.35.0", "author": [ { "name": "William Mantly", diff --git a/nodejs/public/lib/js/app-base.js b/nodejs/public/lib/js/app-base.js index 88f4290..fdad869 100644 --- a/nodejs/public/lib/js/app-base.js +++ b/nodejs/public/lib/js/app-base.js @@ -466,13 +466,19 @@ app.permission = (function(app){ }); } + function update(id, args, callback){ + app.api.put('permission/' + encodeURIComponent(id), args, function(error, data){ + callback(error, data); + }); + } + function remove(id, callback){ app.api.delete('permission/' + encodeURIComponent(id), function(error, data){ callback(error, data); }); } - return {list, subjects, add, remove}; + return {list, subjects, add, update, remove}; })(app); diff --git a/nodejs/routes/permission.js b/nodejs/routes/permission.js index da713e5..9e3c287 100644 --- a/nodejs/routes/permission.js +++ b/nodejs/routes/permission.js @@ -57,6 +57,47 @@ router.post('/', async function(req, res, next){ } }); +// Edit an existing grant. +// +// The record id is derived from (subjectType, subject, scope, domain) +// -- Permission.mkId -- so changing any of those is a DIFFERENT record, not an +// in-place update. Changing only the role is a true update. Handle both here so +// the UI can offer a single "edit" instead of making the operator delete and +// re-add, and so a subject/scope change can never leave the old grant behind +// still conferring access. +router.put('/:id', async function(req, res, next){ + try{ + let existing = await Permission.get(req.params.id); + if(!existing) return res.status(404).json({message: `Permission ${req.params.id} not found.`}); + + let next_ = { + subjectType: req.body.subjectType !== undefined ? req.body.subjectType : existing.subjectType, + subject: req.body.subject !== undefined ? req.body.subject : existing.subject, + scope: req.body.scope !== undefined ? req.body.scope : existing.scope, + domain: req.body.domain !== undefined ? req.body.domain : existing.domain, + role: req.body.role !== undefined ? req.body.role : existing.role, + created_by: reqUsername(req), + }; + if(next_.scope === 'global') next_.domain = '*'; + + // create() upserts on the new id, so this is safe in either direction; + // remove the old record afterwards only when the identity actually moved. + let permission = await Permission.create(next_); + let newId = Permission.mkId(next_); + if(newId !== req.params.id){ + try{ await existing.remove(); }catch(error){ /* already replaced */ } + } + + return res.json({ + message: `Updated ${next_.subjectType} "${next_.subject}" to ${next_.role}` + + (next_.scope === 'global' ? ' globally.' : ` on ${next_.domain}.`), + ...permission, + }); + }catch(error){ + next(error); + } +}); + router.delete('/:id', async function(req, res, next){ try{ let permission = await Permission.get(req.params.id); diff --git a/nodejs/views/permissions.ejs b/nodejs/views/permissions.ejs index 64673b6..ddece57 100644 --- a/nodejs/views/permissions.ejs +++ b/nodejs/views/permissions.ejs @@ -6,11 +6,20 @@