From 8db00f0ed60512fab1151e53dd9f2fe16187e437 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Tue, 4 Aug 2026 19:31:52 -0400 Subject: [PATCH] fix: drop legacy app_super_admin -- SUPER_ADMIN_GROUP is now god_admin (v1.26.1) (#167) 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. --- CHANGELOG.md | 3 +++ docker-entrypoint.sh | 27 ++++++++------------------- nodejs/package-lock.json | 4 ++-- nodejs/package.json | 2 +- nodejs/routes/api_directory_admin.js | 6 +++--- nodejs/utils/permission.js | 15 ++++++++------- 6 files changed, 25 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5491ca..14ca780 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# v1.26.1 +- fix: the legacy `app_super_admin` group is gone โ€” `SUPER_ADMIN_GROUP` (nested into every resource's `_admin` group by auto-provisioning) is now `god_admin`, and `docker-entrypoint.sh` no longer seeds or nests `app_super_admin` (god_admin is nested into the `app_sso_*` groups directly). `isSuperAdmin` still recognizes a pre-existing `app_super_admin` as a migration alias, so an old deployment isn't stripped of rights until it's rebuilt. + # v1.26.0 - feat: complete the group model (docs/GROUPS.md) โ€” `god_admin` is now seeded into LDAP and nested into `app_super_admin`; every site auto-provisions `{site}_super_admin`, `{site}_hosts_*`/`{site}_apps_*` aggregates and `{site}_everyone`; per-resource `_admin`/`_access` groups (named `{site}_{slug}_{level}`, the kind carried in the resource slug) are nested into the site aggregates so the inheritance lattice exists in LDAP, not just in the resolver. Site/aggregate groups are self-healed idempotently on every Directory load, so a directory seeded by an older release picks them up without a rebuild. - feat: the naming convention is now enforced server-side โ€” `POST /api/directory-admin/groups` rejects a group CN that isn't a valid group for the target resource (its own `_admin`/`_access`/capability, a site aggregate, a site-level group, or `god_admin`), so the free-text field can no longer mint `*_accessmember`-style names diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index c3f30c7..f44fd1c 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -359,7 +359,7 @@ EOF # god_admin is the global super group (docs/GROUPS.md ยง2), the top of the # group-inheritance lattice. It is seeded here so it exists from first boot; # the theta-suite bootstrap puts the first admin person into it. - for group in god_admin app_super_admin app_sso_admin app_sso_invite app_sso_oauth_admin app_sso_service_account; do + for group in god_admin app_sso_admin app_sso_invite app_sso_oauth_admin app_sso_service_account; do ldapadd -x -D "$LDAP_BIND_DN" -w "$LDAP_ADMIN_PASS" -H ldap://localhost:389 << EOF || true dn: cn=${group},ou=groups,${LDAP_BASE_DN} objectClass: groupOfNames @@ -370,11 +370,11 @@ member: ${LDAP_BIND_DN} EOF done - # Nest app_super_admin into the SSO admin groups, so cross-app super admins - # hold those rights by membership rather than by a special case in app code. - # This is what makes the privilege visible to every consumer -- SSSD, sudo, - # anything binding LDAP directly -- instead of only to callers that happen - # to route through utils/permission.js. + # Nest god_admin into the SSO admin groups, so god admins hold those rights + # by membership rather than by a special case in app code. This is what makes + # the privilege visible to every consumer -- SSSD, sudo, anything binding + # LDAP directly -- instead of only to callers that happen to route through + # utils/permission.js. # # app_sso_service_account is deliberately excluded: it is a marker for # non-person accounts, not a permission, and nesting admins into it would @@ -385,21 +385,10 @@ EOF dn: cn=${group},ou=groups,${LDAP_BASE_DN} changetype: modify add: member -member: cn=app_super_admin,ou=groups,${LDAP_BASE_DN} -EOF - done - info "Nested app_super_admin into the SSO admin groups" - - # god_admin is the top of the lattice; nesting it into app_super_admin - # (which is itself nested into the app_sso_* groups above) makes it - # resolve to everything app_super_admin holds at the LDAP level too. - ldapmodify -x -D "$LDAP_BIND_DN" -w "$LDAP_ADMIN_PASS" -H ldap://localhost:389 >/dev/null 2>&1 << EOF || true -dn: cn=app_super_admin,ou=groups,${LDAP_BASE_DN} -changetype: modify -add: member member: cn=god_admin,ou=groups,${LDAP_BASE_DN} EOF - info "Nested god_admin into app_super_admin" + done + info "Nested god_admin into the SSO admin groups" fi info "LDAP directory initialized" diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 6bb7b47..acf5544 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -1,12 +1,12 @@ { "name": "t42-sso-manager", - "version": "1.26.0", + "version": "1.26.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "t42-sso-manager", - "version": "1.26.0", + "version": "1.26.1", "license": "MIT", "dependencies": { "@fortawesome/fontawesome-free": "^7.3.0", diff --git a/nodejs/package.json b/nodejs/package.json index 1e5abc8..36c5aac 100755 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -1,6 +1,6 @@ { "name": "t42-sso-manager", - "version": "1.26.0", + "version": "1.26.1", "description": "A very simple LDAP management and SSO system", "author": [ { diff --git a/nodejs/routes/api_directory_admin.js b/nodejs/routes/api_directory_admin.js index 5695cdd..9d84223 100644 --- a/nodejs/routes/api_directory_admin.js +++ b/nodejs/routes/api_directory_admin.js @@ -12,7 +12,7 @@ const groups = require('../utils/groups'); // Make `childCn` a member of `parentCn`, i.e. everyone in the child is // transitively in the parent. Idempotent and non-fatal: "already a member" is -// the goal state, and a missing group (e.g. app_super_admin absent on a +// the goal state, and a missing group (e.g. god_admin absent on a // directory seeded by an older entrypoint) is a reason to skip, not to fail the // caller's real work. async function nestGroup(childCn, parentCn) { @@ -121,7 +121,7 @@ async function ensureSiteGroups(siteSlug, ownerDn, siteName, siteResourceId) { // {site}_{slug}_admin -> {site}_{slug}_access // {site}_{slug}_admin -> {site}_{kind}s_admin (aggregate) // {site}_{slug}_access -> {site}_{kind}s_access (aggregate) -// app_super_admin -> {site}_{slug}_admin (legacy cross-app) +// god_admin -> {site}_{slug}_admin (global super admin) async function provisionResourceGroups(resource, kind, siteSlug, ownerDn) { const accessCn = groups.resourceGroupCns(siteSlug, resource.slug, 'access'); const adminCn = groups.resourceGroupCns(siteSlug, resource.slug, 'admin'); @@ -487,7 +487,7 @@ router.get('/access-summary', async (req, res, next) => { // // Counts come from the transitive closure, not from `member`. Reading the // attribute would report only who is listed on the group, missing anyone - // who reaches it through a nested group -- and since app_super_admin is + // who reaches it through a nested group -- and since god_admin is // nested into every resource's _admin group, that is not an edge case. let members = []; if (group) { diff --git a/nodejs/utils/permission.js b/nodejs/utils/permission.js index 68e7d19..079da48 100644 --- a/nodejs/utils/permission.js +++ b/nodejs/utils/permission.js @@ -4,13 +4,14 @@ 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'; +// (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.