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