Files
proxy/nodejs/routes/permission.js
T
wmantly 255835af7a
Pull Request Tests / Run Tests (18.x) (push) Successful in 49s
Pull Request Tests / Run Tests (20.x) (push) Successful in 40s
Pull Request Tests / Run Tests (22.x) (push) Successful in 41s
Pull Request Tests / Test Summary (push) Successful in 4s
feat: edit permission entries (v1.35.0)
The Permissions page only offered Delete, so changing a role or scope
meant removing the grant and re-adding it from memory.

A permission's id is derived from (subjectType, subject, scope, domain),
so changing any of those is a different record rather than an update. The
new PUT 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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:34:11 -04:00

112 lines
3.9 KiB
JavaScript

'use strict';
const router = require('express').Router();
const conf = require('@simpleworkjs/conf');
const {Permission} = require('../models/permission');
const {LocalGroup} = require('../models/local_group');
const {User} = require('../models').models;
const {reqUsername} = require('../middleware/authz');
// All permission management is admin-only; the gate is applied where this router
// is mounted (routes/api.js).
router.get('/', async function(req, res, next){
try{
return res.json({results: await Permission.listDetail()});
}catch(error){
next(error);
}
});
// Autocomplete source for the "Subject" field: known usernames and group names.
// Groups are derived (no group registry beyond local groups): local groups +
// group-subjects already used in permissions + conf.auth admin/role-map groups.
router.get('/subjects', async function(req, res, next){
try{
let users = (await User.list()) || [];
let groups = new Set();
try{
for(let g of await LocalGroup.list()) groups.add(g);
}catch(error){ /* none */ }
try{
for(let p of await Permission.listDetail()){
if(p.subjectType === 'group' && p.subject) groups.add(p.subject);
}
}catch(error){ /* none */ }
for(let g of (conf.auth && conf.auth.adminGroups) || []) groups.add(g);
for(let g of Object.keys((conf.auth && conf.auth.groupRoleMap) || {})) groups.add(g);
return res.json({users, groups: [...groups].sort()});
}catch(error){
next(error);
}
});
router.post('/', async function(req, res, next){
try{
req.body.created_by = reqUsername(req);
let permission = await Permission.create(req.body);
return res.json({
message: `Granted ${req.body.role} to ${req.body.subjectType} "${req.body.subject}"` +
(req.body.scope === 'global' ? ' globally.' : ` on ${req.body.domain}.`),
...permission,
});
}catch(error){
next(error);
}
});
// 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);
await permission.remove();
return res.json({message: `Permission ${req.params.id} removed.`});
}catch(error){
next(error);
}
});
module.exports = router;