Files
sso-manager-node/nodejs/utils/permission.js
wmantly f00d311029 fix: keep SUPER_ADMIN_GROUP as app_super_admin so resource auto-provisioning nesting works
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>
2026-08-04 16:31:00 -04:00

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, ...)
};