f00d311029
api_directory_admin nests permission.SUPER_ADMIN_GROUP into every new resource's _admin group. Changing it to the not-yet-existing 'god_admin' made that nesting no-op, leaving the creator as the sole member (so the access_request test's beforeAll could not remove the last member of a groupOfNames). Revert it to 'app_super_admin' and recognize 'god_admin' separately in isSuperAdmin + isAdmin. Co-Authored-By: Claude <noreply@anthropic.com>
104 lines
4.1 KiB
JavaScript
104 lines
4.1 KiB
JavaScript
'use strict';
|
|
|
|
const {Group} = require('../models/group_ldap');
|
|
const groups = require('./groups');
|
|
|
|
// The group nested into every resource's _admin group by api_directory_admin
|
|
// (cross-resource super-admin administration). KEEP the legacy `app_super_admin`
|
|
// here: it is the group that actually exists and gets nested. The new schema's
|
|
// global `god_admin` is recognized in isSuperAdmin() below, and api_directory_admin
|
|
// nests SUPER_ADMIN_GROUP -- so until `god_admin` is created during bootstrap, this
|
|
// must stay `app_super_admin` or resource auto-provisioning's nesting silently
|
|
// no-ops (leaving only the creator as the group's sole member).
|
|
const SUPER_ADMIN_GROUP = 'app_super_admin';
|
|
const LEGACY_SUPER_ADMIN_ALIASES = ['app_super_admin'];
|
|
|
|
// True if the user (by resolved member cns) is a global god/super admin.
|
|
// Recognizes BOTH the new schema's `god_admin` and the legacy `app_super_admin`.
|
|
async function isSuperAdmin(memberOfCns) {
|
|
return memberOfCns.includes(groups.GOD_ADMIN) ||
|
|
memberOfCns.some((cn) => LEGACY_SUPER_ADMIN_ALIASES.includes(cn));
|
|
}
|
|
|
|
let byGroup = async function(user, checkGroups, ownerOf){
|
|
// Membership is resolved once, transitively: a user placed in an admin group
|
|
// through a nested group is as much a member as one listed on it directly.
|
|
// Checking `group.member.includes(user.dn)` per group -- as this used to --
|
|
// only ever sees the literal member list and would deny them.
|
|
let memberOfCns = [];
|
|
try{
|
|
memberOfCns = await Group.list(user.dn);
|
|
}catch(error){
|
|
// Fall through to the per-group checks below rather than hard-failing;
|
|
// they still catch direct membership if the resolver is unavailable.
|
|
}
|
|
|
|
if(await isSuperAdmin(memberOfCns)) return true;
|
|
|
|
for(let group of checkGroups){
|
|
if(memberOfCns.includes(group)) return true;
|
|
}
|
|
|
|
// `owner` is deliberately NOT transitive. It designates accountable people,
|
|
// and inheriting ownership through a nested group would hand approval rights
|
|
// to anyone transitively in it -- an escalation nobody asked for.
|
|
for(let group of ownerOf || []){
|
|
try{
|
|
group = await Group.get(group);
|
|
if(group.owner.includes(user.dn)) return true
|
|
}catch(error){
|
|
// group not found, continue checking
|
|
}
|
|
}
|
|
|
|
let error = new Error('Insufficient Permission');
|
|
error.name = 'Insufficient Permission';
|
|
error.message = `You do not have permission to perform this action.`;
|
|
error.status = 401;
|
|
throw error;
|
|
}
|
|
|
|
// Resolve whether a user has `level` on a directory resource under the group
|
|
// model (see utils/groups.js). Applies the inheritance lattice and the
|
|
// `everyone`/`{site}_everyone` meta grants when the resource grants them.
|
|
//
|
|
// user: the auth user ({ dn, isMachine }).
|
|
// resource:{ site, kind: 'host'|'app', slug }.
|
|
// level: 'admin' | 'access' | an opaque capability token.
|
|
// grantedGroups: optional array of the resource's granted group cns (used only
|
|
// for meta `everyone` handling). Omit to skip meta grants.
|
|
async function onResource(user, resource, level, grantedGroups) {
|
|
let memberOfCns = [];
|
|
try { memberOfCns = await Group.list(user.dn); } catch (e) { /* ignore */ }
|
|
|
|
if (await isSuperAdmin(memberOfCns)) return true;
|
|
if (groups.hasPermission(memberOfCns, resource, level)) return true;
|
|
|
|
// Meta grants: `everyone` / `{site}_everyone` confer access to any
|
|
// authenticated (non-machine) user when the resource grants them.
|
|
if (level === 'access' && !user.isMachine && Array.isArray(grantedGroups)) {
|
|
const siteEveryone = groups.siteEveryoneCns(resource.site);
|
|
if (grantedGroups.includes('everyone') || grantedGroups.includes(siteEveryone)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Like onResource but throws Insufficient Permission when denied — for guards.
|
|
async function requireResource(user, resource, level, grantedGroups) {
|
|
if (await onResource(user, resource, level, grantedGroups)) return;
|
|
const error = new Error('Insufficient Permission');
|
|
error.name = 'Insufficient Permission';
|
|
error.status = 401;
|
|
throw error;
|
|
}
|
|
|
|
module.exports = {
|
|
byGroup,
|
|
onResource,
|
|
requireResource,
|
|
isSuperAdmin,
|
|
SUPER_ADMIN_GROUP,
|
|
LEGACY_SUPER_ADMIN_ALIASES,
|
|
...groups, // group schema builders (slugify, resourceGroupCns, ...)
|
|
};
|