Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60ed6b462f | |||
| 255835af7a |
@@ -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 <token>`, not the `auth-token` header — the latter is for browser session UUIDs and would be rejected for a minted API token.
|
||||
|
||||
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