Files
sso-manager-node/nodejs/utils/permission.js
wmantly 2333a145cc
Pull Request Tests / Run Tests (18.x) (push) Failing after 1m38s
Pull Request Tests / Run Tests (20.x) (push) Failing after 25s
Pull Request Tests / Run Tests (22.x) (push) Failing after 26s
Pull Request Tests / Test Summary (push) Failing after 4s
fix: drop legacy app_super_admin -- SUPER_ADMIN_GROUP is now god_admin (v1.26.1)
god_admin now exists at boot (seeded by docker-entrypoint), so the canonical
cross-resource super group nested into every resource's _admin group is god_admin,
not the legacy app_super_admin. docker-entrypoint no longer seeds or nests
app_super_admin (god_admin nests into the app_sso_* groups directly). isSuperAdmin
still recognizes a pre-existing app_super_admin as a migration alias until rebuild.
2026-08-04 19:27:54 -04:00

105 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). This is `god_admin` -- the global
// super group of the new model (docs/GROUPS.md), seeded by docker-entrypoint.sh.
// It used to be the legacy `app_super_admin`, which existed while god_admin
// didn't; now that god_admin is created at boot, the provisioning nests it.
// LEGACY_SUPER_ADMIN_ALIASES still recognizes a `app_super_admin` that predates
// the migration, so an existing deployment isn't stripped of rights until it's
// rebuilt.
const SUPER_ADMIN_GROUP = 'god_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, ...)
};