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>
This commit is contained in:
Generated
+2
-2
@@ -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",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "proxy-api",
|
||||
"version": "1.34.0",
|
||||
"version": "1.35.0",
|
||||
"author": [
|
||||
{
|
||||
"name": "William Mantly",
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -6,11 +6,20 @@
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
// Kept alongside the rendered scope so the Edit modal can pre-fill from the
|
||||
// record without a second round-trip ($.scope exposes push/empty/remove,
|
||||
// not a lookup by key).
|
||||
var permissionsById = {};
|
||||
|
||||
function loadPermissions() {
|
||||
app.permission.list(function(error, data){
|
||||
if(error) return app.messages.action(error, $('#permissions-list'), 'danger');
|
||||
permissionsById = {};
|
||||
$.scope.Permission.empty();
|
||||
for(let p of (data.results || [])) $.scope.Permission.push(p);
|
||||
for(let p of (data.results || [])){
|
||||
permissionsById[p.id] = p;
|
||||
$.scope.Permission.push(p);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -82,6 +91,76 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Edit an existing grant. A permission's id is derived from
|
||||
// (subjectType, subject, scope, domain), so changing any of those replaces
|
||||
// the record rather than updating it -- the API handles that and removes the
|
||||
// superseded grant, which is why this is a single Edit rather than making
|
||||
// the operator delete and re-add (and risk leaving the old grant in place).
|
||||
function permissionEditOpen(id){
|
||||
var p = permissionsById[id] || {};
|
||||
function sel(v, want){ return v === want ? ' selected' : ''; }
|
||||
app.modal.open({title: 'Edit Permission', bodyHtml:
|
||||
'<div class="mb-3">'
|
||||
+ '<label class="form-label fw-bold">Subject type</label>'
|
||||
+ '<select class="form-select" id="perm-edit-subjectType">'
|
||||
+ '<option value="user"' + sel(p.subjectType, 'user') + '>User</option>'
|
||||
+ '<option value="group"' + sel(p.subjectType, 'group') + '>Group</option>'
|
||||
+ '</select>'
|
||||
+ '</div>'
|
||||
+ '<div class="mb-3">'
|
||||
+ '<label class="form-label fw-bold">Subject (username or group)</label>'
|
||||
+ '<input type="text" class="form-control" id="perm-edit-subject" list="subjectUsers" autocomplete="off" value="' + app.util.escapeHtml(p.subject || '') + '" />'
|
||||
+ '</div>'
|
||||
+ '<div class="mb-3">'
|
||||
+ '<label class="form-label fw-bold">Scope</label>'
|
||||
+ '<select class="form-select" id="perm-edit-scope">'
|
||||
+ '<option value="domain"' + sel(p.scope, 'domain') + '>Domain</option>'
|
||||
+ '<option value="global"' + sel(p.scope, 'global') + '>Global</option>'
|
||||
+ '</select>'
|
||||
+ '</div>'
|
||||
+ '<div class="mb-3">'
|
||||
+ '<label class="form-label fw-bold">Domain (for domain scope)</label>'
|
||||
+ '<input type="text" class="form-control" id="perm-edit-domain" autocomplete="off" value="' + app.util.escapeHtml(p.domain || '') + '" />'
|
||||
+ '<div class="form-text text-muted">'
|
||||
+ 'Wildcards: <code>*.example.com</code> matches one label, '
|
||||
+ '<code>**.example.com</code> matches any depth (incl. the apex), '
|
||||
+ '<code>**</code> matches every domain.'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '<div class="mb-3">'
|
||||
+ '<label class="form-label fw-bold">Role</label>'
|
||||
+ '<select class="form-select" id="perm-edit-role">'
|
||||
+ '<option value="viewer"' + sel(p.role, 'viewer') + '>Viewer (read)</option>'
|
||||
+ '<option value="manager"' + sel(p.role, 'manager') + '>Manager (full over domain)</option>'
|
||||
+ '<option value="admin"' + sel(p.role, 'admin') + '>Admin (global only)</option>'
|
||||
+ '</select>'
|
||||
+ '</div>'
|
||||
+ '<hr />'
|
||||
+ '<div class="d-flex justify-content-end gap-2">'
|
||||
+ '<button type="button" class="btn btn-secondary" onclick="app.modal.close()">Cancel</button>'
|
||||
+ '<button type="button" class="btn btn-primary" onclick="permissionEditSave(\'' + id + '\')">Save changes</button>'
|
||||
+ '</div>',
|
||||
});
|
||||
}
|
||||
|
||||
function permissionEditSave(id){
|
||||
var payload = {
|
||||
subjectType: $('#perm-edit-subjectType').val(),
|
||||
subject: ($('#perm-edit-subject').val() || '').trim(),
|
||||
scope: $('#perm-edit-scope').val(),
|
||||
domain: ($('#perm-edit-domain').val() || '').trim(),
|
||||
role: $('#perm-edit-role').val(),
|
||||
};
|
||||
if(!payload.subject){
|
||||
return app.messages.toast('Subject is required', 'warning');
|
||||
}
|
||||
app.permission.update(id, payload, function(error, data){
|
||||
if(error) return app.messages.action((data && data.message) || error, $('#permissions-list'), 'danger');
|
||||
app.modal.close();
|
||||
loadPermissions();
|
||||
});
|
||||
}
|
||||
|
||||
function removePermission(id){
|
||||
app.permission.remove(id, function(error, data){
|
||||
if(error) return app.messages.action(error, $('#permissions-list'), 'danger');
|
||||
@@ -148,7 +227,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="permissionEditOpen('{{id}}')">
|
||||
<i class="fa-solid fa-pen me-1"></i> Edit
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removePermission('{{id}}')">
|
||||
<i class="fa-solid fa-trash me-1"></i> Delete
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user