feat: edit permission entries (v1.35.0)
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

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:
2026-08-06 10:34:11 -04:00
parent 954cda5844
commit 255835af7a
6 changed files with 139 additions and 6 deletions
+84 -2
View File
@@ -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>