From a77aa8d2dfa09344ab050b15d4bc577d391d537d Mon Sep 17 00:00:00 2001 From: William Mantly Date: Tue, 4 Aug 2026 19:10:32 -0400 Subject: [PATCH] feat: seed god_admin + docker plugin, fix ldap-client enrollment, roll up sso v1.26.0 + theta-agent v1.3.0 (v1.36.0) (#161) - bootstrap: seed god_admin into the admin's groups; seed a docker-local discovery plugin - setup.sh: generate ldap-client/ldap.vars from the stack config so LDAP enrollment works - docs: GROUPS.md site-slug convention (verbatim, kind in resource slug) - gitlinks: sso-manager-node 8a9de94 (v1.26.0), theta-agent 52379c2 (v1.3.0) --- CHANGELOG.md | 13 ++++++++ bootstrap/bootstrap.js | 69 +++++++++++++++++++++++++++++++++++++++++- docs/GROUPS.md | 19 +++++++++--- setup.sh | 22 ++++++++++++++ sso-manager-node | 2 +- theta-agent | 2 +- 6 files changed, 120 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e568633..309856b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,19 @@ orchestration code; see each submodule's own `CHANGELOG.md` [sso-manager-node](https://github.com/theta42/sso-manager-node/blob/master/CHANGELOG.md)) for what changed inside the apps it composes. +## [v1.36.0] - 2026-08-04 + +### Added +- **`god_admin` seeded + site groups auto-provisioned** (sso v1.26.0) — `god_admin` exists from first boot; every site gets `{site}_super_admin`, `{site}_hosts_*`/`{site}_apps_*` aggregates and `{site}_everyone`; per-resource groups (`{site}_{slug}_{level}`) nest into the site aggregates (the inheritance lattice now exists in LDAP, not just the resolver). See the sso changelog for the full group-model completeness + server-side naming enforcement + Directory god_admin management. +- **Docker discovery plugin configured out of the box** — the bootstrap seeds a `docker-local` plugin instance pointed at `/var/run/docker.sock`, so a fresh stack discovers its own containers into the Directory immediately (idempotent; an operator-created instance is left alone). + +### Fixed +- **ldap-client enrollment no longer fails** — `setup.sh` was calling `ldap-client/index.sh`, which refuses to run without a gitignored `ldap.vars` that nothing ever created (the "ldap.vars file not found!" + "enrollment failed" you saw). It now generates `ldap-client/ldap.vars` from the stack's own config (LDAPS host, base DN, `cn=ldapclient` bind + service password, SSO URL, site name) before enrolling; an operator-provided `ldap.vars` is always kept. +- **theta-agent no longer logs `Unknown command type: heartbeat_ack`** every minute — the server's ack of the agent's own heartbeat is now silently ignored instead of falling through to the unknown-command handler (which also answered with a spurious error). + +### Changed +- **Roll up sso v1.26.0 + theta-agent v1.3.0** — gitlinks point at the version-tagged commits for both submodules (sso-manager-node → 8a9de94, theta-agent → 52379c2). Full changelogs: [sso](https://github.com/theta42/sso-manager-node/blob/master/CHANGELOG.md), [theta-agent](https://github.com/theta42/theta-agent/blob/master/CHANGELOG.md). + ## [v1.35.18] - 2026-08-04 ### Changed diff --git a/bootstrap/bootstrap.js b/bootstrap/bootstrap.js index 398b4f4..688b992 100644 --- a/bootstrap/bootstrap.js +++ b/bootstrap/bootstrap.js @@ -89,7 +89,12 @@ const CLIENT_NAME = 'theta-proxy'; const ADMIN_DN = `cn=${ADMIN_UID},ou=people,${BASE_DN}`; const SVC_DN = `cn=ldapclient,ou=people,${BASE_DN}`; -const ADMIN_GROUPS = ['app_sso_admin', 'app_sso_oauth_admin']; +// god_admin is the global super group (docs/GROUPS.md §2); the bootstrapped +// admin is its first member. app_sso_admin / app_sso_oauth_admin are the legacy +// per-console admin groups still used by the SSO UI. god_admin is nested into +// app_super_admin by docker-entrypoint.sh, so LDAP-level consumers (SSSD, sudo) +// resolve it transitively. +const ADMIN_GROUPS = ['god_admin', 'app_sso_admin', 'app_sso_oauth_admin']; const log = (...a) => process.stderr.write('[bootstrap] ' + a.join(' ') + '\n'); const out = (k, v) => process.stdout.write(`${k}=${v}\n`); @@ -537,6 +542,59 @@ async function seedDirectory(token, clientId, jumpClientId) { await linkOauthClient(jumpClientId, jumpSvc, 'jump-host'); } +// ── Plugin instances ──────────────────────────────────────────────────────── +// Seed a sensible default set of plugin instances so the stack is usable the +// moment it boots, without the operator having to add them by hand. The setup +// stack runs on Docker, so the single biggest win is a Docker discovery plugin +// pointed at the local daemon socket: containers that make up the stack (and +// any others on the host) get discovered into the Directory automatically. +// Idempotent per slug: an instance an operator already created is left alone. +async function seedPlugins(token) { + async function pluginGet(path) { + const res = await fetch(`${SSO_INTERNAL}/api/plugins/${path}`, { + headers: { 'auth-token': token }, + }); + if (!res.ok) throw new Error(`GET /api/plugins/${path} failed (${res.status})`); + return res.json(); + } + async function pluginPost(body) { + const res = await fetch(`${SSO_INTERNAL}/api/plugins/`, { + method: 'POST', + headers: { 'auth-token': token, 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(`POST /api/plugins failed (${res.status}): ${text}`); + } + return res.json(); + } + + async function ensurePlugin({ pluginType, name, slug, config }) { + const existing = ((await pluginGet('')).results) || []; + if (existing.some((i) => i.slug === slug)) { + log(` plugins: '${slug}' exists — keeping`); + return; + } + await pluginPost({ pluginType, name, slug, config }); + log(` plugins: created '${slug}' (${pluginType})`); + } + + try { + // The Docker daemon the setup stack itself runs under. The socket must be + // mounted into the sso container for discovery to reach it; if it isn't, + // discovery simply errors non-fatally until it is. + await ensurePlugin({ + pluginType: 'docker', + name: 'Local Docker daemon', + slug: 'docker-local', + config: { socketPath: '/var/run/docker.sock' }, + }); + } catch (e) { + log(`WARNING: plugin seed failed (${e.message || e}) — continuing`); + } +} + // Write the OAuth client creds back into /config/proxy-secrets.js so the proxy // (which reads that file) can use them. Only the clientId/clientSecret lines // are touched; the rest of the file (operator edits, comments) is preserved. @@ -786,6 +844,15 @@ async function provisionJumpHost(token) { log(`WARNING: directory seed failed (${e.message || e}) — continuing`); } + // Seed default plugin instances (Docker discovery) — same warn-and-go + // policy; a stack without plugins is still usable. + try { + log('Seeding default plugins...'); + await seedPlugins(token); + } catch (e) { + log(`WARNING: plugin seed failed (${e.message || e}) — continuing`); + } + log('Done.'); process.exit(0); } catch (e) { diff --git a/docs/GROUPS.md b/docs/GROUPS.md index a00b674..b5fb0f3 100644 --- a/docs/GROUPS.md +++ b/docs/GROUPS.md @@ -76,9 +76,20 @@ enumerated as LDAP members, and cannot be used as Unix groups. - The **structural delimiter is `_`**. It appears only between the fixed segments of a group name. -- **Site, host, and app slugs never contain `_`.** Normalize to lowercase; - spaces and `_` → `-`; strip other non-`[a-z0-9-]`. A host named `Web 01` and a - site `Main Office` produce slugs `web-01` and `main-office`. +- **The `S` site segment is the site resource's slug verbatim.** In the SSO + Directory, site/host resource slugs carry a kind prefix (`site_local`, + `host_theta-env`); the group builders keep them verbatim rather than + re-slugifying (which would corrupt the delimiter: `site_local` → `site-local`) + or inserting a separate kind segment. So a host resource `host_theta-env` under + site `site_local` yields `site_local_host_theta-env_access` (the `host_` is part + of the resource slug), and the site's own admin group is `site_local_super_admin`. + Services are stored without a prefix, giving `site_local_sso-manager_access`. + The kind (`host`/`app`) is used only to pick the **aggregate** the resource's + group nests into (`{site}_hosts_*` / `{site}_apps_*`), not the resource's own + group name. +- **Within a segment, normalize to lowercase** — spaces and stray `_` → `-`; strip + other non-`[a-z0-9-]`. A host named `Web 01` and a site `Main Office` (resource + slugs `host_web-01` and `site_main-office`) yield groups `site_main-office_host_web-01_*`. - **Aggregate groups use the plural kind** (`hosts`, `apps`); per-resource groups use the singular (`host`, `app`). This makes `S_hosts_admin` unambiguous even if a host were named `admin` (that host would be `S_host_admin_admin`). @@ -139,7 +150,7 @@ def effective(resource, level_or_cap, site): if level_or_cap in ("admin","access"): agg = f"{site}_{resource.kind}s_{level_or_cap}" if user in agg: return True - specific = f"{site}_{resource.kind}_{resource.slug}_{level_or_cap}" + specific = f"{site}_{resource.slug}_{level_or_cap}" # slug carries its kind if user in specific: return True if level_or_cap == "access": return effective(resource, "admin", site) if level_or_cap == "admin": return False # access does not imply admin diff --git a/setup.sh b/setup.sh index ada2fd8..f9a28bf 100755 --- a/setup.sh +++ b/setup.sh @@ -1257,6 +1257,28 @@ if [[ "$CFG_THETA_AGENT_ENABLE" == "1" ]] && [[ -x /usr/local/bin/theta-agent ]] if [[ "$CFG_THETA_AGENT_LDAP_AUTH" == "1" ]]; then info " Configuring LDAP authentication for this host..." + + # ldap-client/index.sh refuses to run without ./ldap.vars, which is + # gitignored and never shipped in the checkout (it holds a real bind + # password). On the agent-enrollment path we generate it from the stack's + # own config so the host can actually enroll; an operator-provided + # ldap.vars (cp ldap.vars.template ldap.vars + edit) is always kept. + if [[ ! -f ldap-client/ldap.vars ]]; then + info " Generating ldap-client/ldap.vars from the stack config..." + cat > ldap-client/ldap.vars <