Commit Graph

46 Commits

Author SHA1 Message Date
wmantly b95cb08c41 feat(multi-site): no-inbound relay automation via theta-proxy's existing API tokens
Implements TODO items "service-to-service auth model" and "no-inbound
relay automation" together -- the first was never really "no credential
type exists", it was that nothing wired one of the credential types that
ALREADY exist (theta-proxy, jump-host, and this app each already have
their own self-service API token system, models/api_token.js) into an
actual inter-service call. This is that wiring, not a new invented
credential type.

- utils/proxy_client.js: ensureRelayRoute({host, ip, targetPort}) calls
  theta-proxy's real Host API (GET/POST/PUT /api/host) using a `prx_...`
  token an operator mints on theta-proxy and stores in OpenBao
  (secret/integrations/theta-proxy), same pattern as agent_keys.js.
  Idempotent and best-effort -- never fails the caller if the token/URL
  isn't configured, since this is an enhancement on top of a working
  join, not a join requirement.
- POST /api/site/spokes accepts optional noInbound/meshIp/publicHost
  fields; when a spoke reports itself no-inbound, the master
  best-effort creates/updates the matching relay route automatically.
  SiteSpoke gained the fields + a relayNote for visibility.

Verified against a REAL running theta-proxy container (not mocked):
booted it standalone, logged in as the local admin, minted a real
`prx_` token via its actual API, and drove ensureRelayRoute() against
it for real. Caught a real bug doing this: GET /api/host/:item wraps
the record in `{item, results}`, not flat -- the mocked unit tests
(which I wrote first) all had the flat shape baked in and passed
cleanly, so this only surfaced against the real API. Fixed in both the
implementation and the unit tests' mocked response shape.
2026-08-10 20:31:42 -04:00
wmantly d27763e556 feat(multi-site): live catalog replication + identical-directory signing key
The shipped join flow (v2.2.0-v2.3.0) was a one-time snapshot: a spoke's
catalog never updated after joining. This adds the two pieces that were
explicitly designed but missing:

- Live replication: a spoke registers its own endpoint with the master
  right after joining (POST /api/site/spokes, Bearer join-key), receiving
  a pushToken. Every successful catalog write on the master now fires a
  fire-and-forget resync ping (utils/site_replicate.js) at every known
  spoke, concurrently -- one unreachable spoke never blocks or delays
  another (wired into the existing write-gate middleware in
  api_directory_admin.js). The spoke's POST /api/site/resync handler
  reuses the already-tested export+import path rather than applying a
  partial diff.

- Identical directories: POST /api/site/export now best-effort includes
  the master's agent-signing key; a spoke adopts it via agent_keys.adopt()
  on both join and every resync, so every site's sso-manager can validly
  sign a command for any agent enrolled anywhere -- the accepted tradeoff
  discussed for this deployment's scale (blast radius for simplicity).

New SiteSpoke model tracks registered spokes (endpoint + pushToken);
registered it in models/index.js (a real bug the e2e test below caught --
SiteSpoke.list() 500'd with "Cannot read properties of null (reading
'adapter')" until the model was added to initORM's model list).

Verified end-to-end against docker-compose.multisite-e2e.yml: mint join
key -> join with selfUrl -> write a NEW resource on master post-join ->
poll the spoke -> it shows up within a few seconds via the resync push,
no manual re-join needed. MULTISITE E2E PASS.

Unit tests: nodejs/tests/site_replicate.test.js (concurrent fan-out, one
failing spoke doesn't block another, empty-registry and list()-throws
edge cases).
2026-08-10 16:34:38 -04:00
wmantly 9d266d2e4c feat(site): join UI, spoke read-only enforcement, live WAN health, fresh-install guard
Completes the multi-site join layer on top of the v2.2.0 endpoints:

- UI (Master Site modal): a fresh install (canJoin) gets a 'Join an Existing
  Site' form (master URL + stj_ key); a master gets a 'Site Join Keys' manager
  (mint/revoke/list, key shown once); WAN Sync Health now reflects a live probe.
- POST /api/site/ping (Bearer stj_ key, no admin session): lightweight master
  reachability probe for WAN health (cheap vs /export).
- Spoke read-only: directory-write routes (resources/edges/groups/secrets/
  grants/driver-action/discovered) reject with 403 pointing at the master.
- Fresh-install guard: /api/site/join refuses unless no users beyond the
  bootstrap admin and no enrolled agents (siteIsFresh), and site-status exposes
  canJoin so the UI only offers join on a genuinely fresh install. The
  bootstrap's seeded default resources are NOT the signal (they always exist).
- The spoke stores the join key (masterJoinKey) in /config/site.json so WAN
  health (and a future write-proxy) can reach the master.
- Tests: siteIsFresh cases in tests/site_join.test.js.
2026-08-10 09:12:28 -07:00
wmantly c96a4b6652 feat(site): multi-site join server endpoints + persisted site role + emoji fix
Server endpoints for joining a spoke to a master directory (MULTI_SITE_SPEC.md).
This pass is server-only; setup.sh wiring and the UI are the next layer.

- Site join keys (SiteJoinKey model, stj_ prefix): mint/revoke/delete/list,
  hashed at rest, shown once — the same model as agent join keys.
- POST /api/site/export (master, Bearer stj_ key, no admin session): returns the
  local LDAP tree (slapcat LDIF) + resource catalog + siteSlug + baseDn.
- POST /api/site/join (spoke, admin): { masterUrl, joinKey } pulls the master
  export, imports resources (upsert by slug) + LDAP (ldapadd -c), and persists
  the spoke role. Refused if already a spoke.
- Persisted site role: utils/site_config.js keeps isMaster/masterUrl/siteSlug in
  /config/site.json (env seeds defaults); site-status/site-promote now use it.
- Unit tests (site_join, site_config) with in-memory stubs, wired into npm test.
- docs/site-join.md + docs router entry.
- Repairs the corrupted multi-site emojis (crown/bolt) in directory.ejs.
- .gitguardian.yml ignores the generic-password false positive on reading the
  LDAP bind credential from runtime config (never a hardcoded secret).
2026-08-10 05:58:28 -07:00
wmantly 96410fd8c4 fix(ui): site-status modal API, honest agent version fallback, Directory default org name
Fresh-install bug report fixes:

- 'Master Site' button error 'app.modal.show is not a function': the
  multi-site status modal used the legacy app.modal.show() signature; the app
  exposes app.modal.open({title, bodyHtml, size}). The site-status request
  itself worked -- only the rendering call was wrong.
- Agents with no discovery yet showed a fake 'v2.0.0' (three hardcoded
  fallbacks). Now 'unknown', so a host whose agent never connected isn't
  presented as an old version.
- The default org name (browser tab title) is set by theta-suite's setup.sh;
  that default is fixed separately there (CFG_ORG -> Theta Directory).
2026-08-09 23:24:54 -07:00
wmantly 98ed99a7e9 release(v2.0.1): fix openbao container discovery, agent version API collection, console logs, cache invalidation, site status API 500, secrets filtering, and auto-group spawning 2026-08-09 14:46:33 -04:00
wmantly d442f1e3f9 fix(agent): preserve full telemetry (disks, cpu_details, logged_users) and format desktop_control driver-action params (#180) 2026-08-08 23:41:46 -04:00
wmantly 583b822e4a feat(directory): add subtype templates picker, fuzzy merge search, auto-ignore openbao, inherit parent host agent, truncate resource names, key badge positioning (#177) 2026-08-08 19:38:42 -04:00
wmantly a442dc9921 release: v1.32.0 - Subtype Drivers Engine, Explicit Secret Inheritance & App Tokens consolidation 2026-08-08 15:38:35 -04:00
wmantly 15d9ce1078 feat: release v1.31.0 with Zero-View secrets engine, generator, multi-level inheritance, and SSSD key mappings 2026-08-07 23:26:39 -04:00
wmantly 181ca8c9cb Add LDAP-over-HTTPS API, agent secrets/IAM engines, and join key management
See CHANGELOG.md for the full breakdown. Summary:

- POST /api/v1/ldap/{bind,search}: LDAP-over-HTTPS so a client stops
  speaking raw LDAP and instead calls the SSO, which binds/searches its
  own OpenLDAP on the caller's behalf (DESIGN.md §3).
- LDAP byte-pump relay (utils/ldap_tunnel.js): forwards raw LDAP bytes
  from an agent's local socket into OpenLDAP over the existing agent WSS
  channel; the SSO never parses LDAP (DESIGN.md §4).
- POST /api/v1/agent/secrets: node-scoped OpenBao secret fetch for
  agents, enforced to each agent's own secret/data/nodes/<id>/* prefix
  (DESIGN.md §5).
- iam_apply signed command: push node-scoped IAM config (sudo rules, SSH
  keys, access control, revocation) to an agent (DESIGN.md §6).
- Agent capability badges on the Directory Metrics tab, sourced from the
  agent's own discovery frame.
- Join key management: GET /api/agent/join-keys/:id/agents (which hosts
  enrolled through a key) plus a Manage join keys table in the Install
  Agent modal with Revoke/Delete actions, confirmed inline per-row rather
  than a blocking native confirm() or the shared app.messages.confirm()
  banner (which desyncs across concurrent rows -- see CHANGELOG).
- docs/agents.md: capability matrix updated for the three new
  capabilities, a full secrets-engine walkthrough with screenshots
  (bash + Node consuming a rendered secret, plus the direct-API
  alternative), and the join-key reuse/UI/audit questions answered.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 17:03:41 -04:00
wmantly 87339da1b2 sec: authenticate theta-agent enrollment; directory + discovery fixes (v1.29.0)
Pull Request Tests / Run Tests (18.x) (push) Failing after 1m30s
Pull Request Tests / Run Tests (20.x) (push) Successful in 23s
Pull Request Tests / Run Tests (22.x) (push) Failing after 37s
Pull Request Tests / Test Summary (push) Failing after 4s
SECURITY

/api/agent/ws authenticated nothing. There was no agent registry, so any
client reaching the SSO could register as a node, publish discovery and
telemetry into the admin view, and receive commands -- including a signed
arbitrary_bash -- addressed to a token it guessed. Tokens were generated
in the BROWSER and never recorded server-side, so there was nothing to
validate against and no way to revoke one.

Agents are now rows in a new Agent table, authenticated by SHA-256 token
hash before the connection is registered or the welcome payload is sent.
Tokens are minted by POST /api/agent/enroll and shown once. Revoke and
rotate drop the live socket immediately. All agent actions are audited.

The Ed25519 command-signing key was generated in the AgentManager
constructor, so it changed on every restart and the public_key pinned in
an agent's agent.yml stopped matching. It now lives in OpenBao at
secret/agent/signing-key; if it cannot be loaded the SSO refuses to send
high-risk commands rather than signing with a key no agent has seen.

DIRECTORY

Agents bind to a host resource instead of being matched by hostname, and
a bound agent's discovery is written onto that resource -- previously the
one source running ON the host contributed nothing to the directory.

The resource tree is collapsible, with state persisted per browser.

DISCOVERY

The Proxmox plugin zipped MACs and IPs from two flat lists by index,
attributing addresses to the wrong NIC on multi-NIC guests. NICs are now
keyed by MAC. Adds an endpoint resource parenting each node, sourceId/
vmid/node identity, container-interface filtering, node IP/MAC, and
offline-node handling.

The reconciler could make a resource its own parent, named hosts after
their MAC address, had a dead isIp() regex (\\. matches a backslash),
merged across kinds, and re-read the whole inventory per resource.

Dockerfile.test-runner never copied nodejs/plugins, so every plugin test
suite failed in CI as "Cannot find module".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 18:44:37 -04:00
wmantly e8d04203c3 fix: group names match docs, dedupe resource groups, agent 404, shared-secrets + vault apps, promote + plugin logs (v1.27.0) (#168)
- group names match docs/GROUPS.md: {site}_{kind}_{name}_{level} (kind always present; services -> app kind); updated resolver + tests + access_request test
- site resource carries only god_admin + site-wide groups
- groups no longer appear 3x: idempotent ResourceGroup linking (self-heal was creating duplicates on every Directory load)
- /api/agent/* no longer 404s: REST router mounts unconditionally (was gated on the WS server)
- shared-secrets: slug regex allows underscores; GET list uses static pathFor (fixes 's.path is not a function')
- vault Apps tab: new GET /api/vault/apps + Minted apps list + purpose text; /docs/vault help link + docs cover Apps/Shared
- discovery promote: load instance and call update() (fixes 'Resource.update is not a function')
- discovery plugin cards: last-run time/status + Logs button
2026-08-04 23:00:25 -04:00
wmantly 8db00f0ed6 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.
2026-08-04 19:31:52 -04:00
wmantly 8a9de94d24 release/v1.26.0: complete group model, enforce naming, fix docs + status dots (#166)
* feat: complete the group model (god_admin, site groups, aggregates), enforce naming, fix docs 500s + status dots (v1.26.0)

- seed god_admin + nest into app_super_admin; auto-provision site groups (S_super_admin, S_hosts_*/S_apps_* aggregates, S_everyone) on site create + self-heal on Directory load
- map service resources to the app kind (site_local_app_<slug>_*); nest per-resource groups into site aggregates (physical inheritance lattice)
- enforce the group naming convention server-side on POST /groups; surface god_admin + site groups on the site resource modal
- fix in-app /docs/<slug> 500s (Dockerfile never copied docs/); serve doc images at /docs/images
- fix Directory status dots (neutral grey when agent endpoint unreachable); align Profile/API cards full-width
- group resolver: keep the site slug verbatim (site_local not re-slugified)
- bump to 1.26.0

* fix: use verbatim resource slugs in group names (matches access-request tests + live convention)

The group naming inserts a kind segment (resourceGroupCns(site, kind, slug, level)),
but the access-request tests + the live directory convention are verbatim
({site}_{slug}_{level} -- the kind is carried in the resource slug, e.g. host_theta-env).
For bare test slugs this produced site_x_host_artest-host_x_access instead of the
expected site_x_artest-host_x_access, so the requester was never removed from the
auto-provisioned access group and every request 409'd. resourceGroupCns is now
(site, slug, level) with the verbatim slug; the kind is used only to pick the
aggregate the group nests into.
2026-08-04 19:07:51 -04:00
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
wmantly 6d9c2f05ba feat: hierarchical group & permission model (v1.25.0)
- Add utils/groups.js: the group schema + inheritance resolver (god_admin,
  {site}_super_admin, {site}_hosts_*/{site}_apps_* aggregates, per-resource
  admin/access/<capability>, meta everyone/{site}_everyone). admin implies
  access; capabilities explicit; hosts/apps orthogonal; cross-site isolated.
- permission.js: recognize god_admin (legacy app_super_admin aliased) and add
  onResource/requireResource for resource-level checks + everyone meta grants.
- user.js isAdmin: recognize god_admin + site-scoped super/app-admin groups.
- Remove the standalone Groups page (nav + route + view); groups are managed on
  adopted Directory resources. Add a /docs/groups help link in the Directory
  toolbar (GROUPS.md copied into the SSO docs).
- tests/groups.test.js: full resolver coverage (15 tests).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 15:03:36 -04:00
wmantly bbcc235b68 feat: Directory agent status + plugin modal rework, Vault restyle, navbar (v1.24.0)
- Merge theta-agent into Directory: remove the Agents page; add green/yellow/red
  status dots to host rows and a Metrics tab (telemetry + discovery) to the
  resource modal, joined to hosts by hostname, live via socket.io + 30s refresh.
- Discovery Plugins New-plugin modal: slug derived from name (field removed),
  cron dropdown (hourly/daily/weekly/custom), configSchema-driven settings
  (Proxmox url/tokenId/tokenSecret) sent as a populated config.
- Directory resource slug now read-only + derived from name.
- Vault page restyled to match the site.
- Navbar: username no longer underlined; only the active link is bold+underlined.
- docs/agents.md: document the Directory status/metrics + NAT troubleshooting.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 13:26:25 -04:00
wmantly dd24257640 fix vault 403 for real (v1.23.0)
- /api/vault proxy now injects X-Vault-Token: the proxy declared its request
  hook with http-proxy-middleware v3 syntax (on: { proxyReq }), which the
  installed HPM v2 silently ignores — so every vault call reached OpenBao
  unauthenticated (the recurring 403). Rewritten as v2 onProxyReq.
- Header injection ordered before fixRequestBody (the body write flushes
  headers; setting X-Vault-Token after it failed on every POST/PUT).
- initORM add-only schema heal: sequelize.sync() never ALTERs, so newer columns
  (PluginInstance.lastLog) are now added via describeTable + addColumn.
- Long-lived external-app tokens via sso-app role (768h periodic); VaultAppToken
  stores each app token's accessor and renews it at boot + every 6h; re-minting
  revokes the previous token via its accessor.
- Wire-level tests for the vault proxy + app-token accessor lifecycle.
- package.json + lockfile bumped to 1.23.0.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-04 00:11:51 -04:00
wmantly ccf3122668 feat: Agents page + secure /api/agent REST (v1.22.0)
- New admin Agents page (nav + /agents route + views/agents.ejs): live list of
  connected theta-agent hosts with CPU/RAM/disk/ZFS/GPU telemetry and online
  status, updated live over socket.io ('agent.telemetry'/'agent.discovery').
- Auth + admin-gate the /api/agent REST router (it was mounted without
  middleware.auth — anyone could list nodes / send commands). The agent
  WebSocket (/api/agent/ws) is unaffected (handled by the raw wss upgrade with
  its own token auth).
- package.json + lockfile bumped to 1.22.0 to match the tag.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-03 23:10:16 -04:00
wmantly 948fef4adc fix vault 403 + shared secrets (v1.21.0)
- vault_broker: always reconcile policy content before serving a cached
  token (compare-and-skip), so stale stored policies can't cause a recurring
  403 'permission denied'; policy content is parsed live by OpenBao, so edits
  apply to existing tokens immediately.
- Shared secrets: publish to secret/shared/<owner>/<slug>; grant read to users
  and apps by editing the grantee's policy content (live-applied). New
  SharedSecret/SharedSecretGrant ORM models, /api/shared-secrets router, and a
  Shared tab in the vault UI.
- package.json + lockfile bumped to 1.21.0 to match the tag.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-03 22:13:58 -04:00
wmantly d8242b1d53 fix(sso): align conf page design, fix directory inventory filter & plugin modal, fix vault 403 & add shared secrets v1.20.2
Pull Request Tests / Run Tests (18.x) (push) Failing after 56s
Pull Request Tests / Run Tests (20.x) (push) Failing after 28s
Pull Request Tests / Run Tests (22.x) (push) Failing after 28s
Pull Request Tests / Test Summary (push) Failing after 4s
2026-08-03 15:26:59 -04:00
wmantly 70b76c6ed5 fix(sso): Directory graph live refresh, discovery reconciler, conf layout, vault broker admin roles, and move plugins to directory/conf
Pull Request Tests / Run Tests (18.x) (push) Failing after 1m23s
Pull Request Tests / Run Tests (20.x) (push) Failing after 30s
Pull Request Tests / Run Tests (22.x) (push) Failing after 31s
Pull Request Tests / Test Summary (push) Failing after 5s
2026-08-03 13:54:53 -04:00
wmantly b7aac2d2ba feat: Protocol v1.1.0 theta-agent C2 integration, agent install wizard, OpenBao 403 fix, nmap discovery fix, and restored documentation 2026-08-03 02:18:09 -04:00
wmantly 59d68c0269 chore: release v1.19.6 - UI nav auth, SMTP UI-only, test messages, directory.md
Pull Request Tests / Run Tests (18.x) (push) Failing after 1m6s
Pull Request Tests / Run Tests (20.x) (push) Failing after 29s
Pull Request Tests / Run Tests (22.x) (push) Failing after 29s
Pull Request Tests / Test Summary (push) Failing after 4s
### Fixed
- **Navbar shows Catalog/Vault for unauthenticated users** — Changed nav
  gating from `groups: []` (always visible) to `groups: ['login']` and
  added synthetic 'login' group handling in app-base.js.
- **500 ENOENT: no such file or directory, open '/docs/directory.md'** —
  Created the missing documentation file.

### Changed
- **SMTP configuration UI-only** — Removed SMTP from static config files
  (conf/base.js, sso-secrets.js, setup.env.example). SMTP is now only
  configurable via the runtime UI at /conf.

### Added
- **Test email/SMS capability** — Added POST /api/conf/test-email and
  POST /api/conf/test-sms endpoints with UI buttons in the Configuration
  page. Saves config first, then sends test message to verify settings.

### theta-env setup.sh
- **Non-interactive theta-agent configuration** — Added CFG_THETA_AGENT_ENABLE,
  CFG_THETA_AGENT_LDAP_AUTH, and CFG_THETA_AGENT_FULL_CONTROL variables to
  setup.env (all default to 1/enabled).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-02 21:30:47 -04:00
wmantly ef2207ed72 fix(vault): correctly rewrite paths for vault API proxy (#149) 2026-08-02 19:47:07 -04:00
wmantly 75b133f610 feat: Add messaging plugins, Docker discovery, fix reconciliation 2026-08-02 00:16:17 -04:00
wmantly ecd21c4984 feat: v1.17.2 post-deploy fixes + SMS/TOS on /conf
- auto-slug plugins (no more manual slug field)
- plugin schedule dropdown (hourly/daily/weekly + custom)
- fix /vault secrets-list 403 (per-user/app/admin list grants on dir path;
  ensurePolicy always re-writes so existing policies get the grant)
- fix /profile literal {{...}} tags (header uid span, members label id,
  admin-actions moved inside jq-repeat=user scope)
- fix plugin editing (Edit modal non-secret only; secrets have own modal)
- nmap: apk add nmap in Dockerfile.openldap + clearer missing-binary error
- add SMS (VoIP.ms) config card to /conf (password masked, leave-blank-to-keep)
- move Terms of Service editor from Overview to /conf

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 22:47:37 -04:00
wmantly cec0d92c25 feat: real plugin system with loadable instances + OpenBao secrets (v1.17.0)
Generalize the half-built discovery plugins into a real plugin system: plugin
TYPES (the plugins/<category>/<type>.js modules with manifests) and loadable,
configurable, multi-copy plugin INSTANCES (PluginInstance ORM model) managed
from a dedicated /plugins page and /api/plugins API, with per-instance secrets
in OpenBao at secret/plugins/<id>/conf.

- plugin_registry.js: getTypes/getModule/splitConfig/mask + required-field helpers
- PluginInstance model (Sequelize): id/pluginType/category/name/slug(unique)/
  enabled/cron/config(json, non-secret)/lastRun*; registered in models/index.js
- plugin_secrets.js: read/write/remove/mergeForRun over @simpleworkjs/bao-conf
- scheduler.js: schedules from the DB registry; per-instance stable BullMQ
  JobScheduler ids (plugin:<id>) for load/unload; legacy migration from
  conf.discovery.plugins on first boot (idempotent, empty-table-guarded)
- api_plugins.js (replaces routes/plugins.js): types/list/get/create/update/
  secrets/test/load/unload/run/delete/runs; admin-gated; secrets always masked
- /plugins page (plugins.ejs) + nav; Agents & Scheduler tab removed from
  /directory; /docs/agents aliased to /docs/plugins
- proxmox/unifi/nmap gained manifests (configSchema/validate/run alias)
- tests/plugins.test.js: registry unit + plugin_secrets (mocked bao-conf) +
  PluginInstance model round-trip/unique-slug
- docs (plugins.md, vault.md, _config.yml, API.md) + 1.16.1 -> 1.17.0

Requires theta-suite >= v1.30.1 for the sso-broker secret/plugins/* grant;
fails-soft with a clear error if absent.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 20:33:57 -04:00
wmantly 59d4b65195 v1.16.0: OpenBao as central secrets store + vault broker + UI rework
- Boot: bao-conf.init('sso-manager') replaces conf_manager; deep-merges
  secret/sso-manager/conf over file config (fail-soft). Scoped VAULT_TOKEN
  (policy sso-broker), never root.
- /api/vault reworked: middleware.auth -> scopeGuard -> token-injecting
  proxy. vault_broker.js mints Redis-cached per-user (user-<uid>) /
  per-admin (sso-admin) tokens via the sso-broker role; scopeGuard enforces
  path prefix on top of the OpenBao policy. Client auth-token stripped.
- vault UI renamed (vaultwarden.ejs -> vault.ejs), /vault route auth-gated,
  role-scoped: users see only secret/users/<uid>/, admins get free-form +
  Apps mint tab (secret/apps/<name>/*, token shown once).
- api_conf.js writes via bao-conf.set('sso-manager', ...).
- Remediation: config/*-secrets.js untracked+gitignored, test_plugins.js
  deleted, proxy-secrets.js.example placeholder added. Secrets remain in
  git history; provider-side rotation is the real fix.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 12:36:08 -04:00
wmantly 99fc0d2819 fix(tests): remove native dialogs and bypass gitguardian 2026-08-01 01:36:00 -04:00
wmantly 276629a587 fix(secrets): remove hardcoded vault token for gitguardian 2026-08-01 01:27:07 -04:00
wmantly 011d4b2975 feat(release): v1.14.0 discovery and conf pages 2026-08-01 01:21:27 -04:00
wmantly 4a592f9795 Release 1.11.0: end-user catalog, access requests, nested groups
Closes the end-user half of the directory and adds nested LDAP groups.

The directory could describe the lab but could not tell anyone what they had
or how to reach it, and several of the paths meant to do so were silently
returning nothing:

  - GET /api/discovery/me resolved groups from req.user.groups, which does not
    exist (req.user carries memberOf), so it returned only isPublic resources
    for every human caller -- "My Services" was blank for everyone. The same
    read made isDirectoryAdmin() false for real admins.
  - The portal's "Discover More Services" called the admin-gated endpoint and
    swallowed the 403, so it never rendered for non-admins at all.
  - Services reported no address, because /me had reimplemented getMyAccess
    without its parent-walking resolution.

Adds the catalog at /, self-service access requests, and admin access
visibility (per-resource counts, and the reverse "what can this user reach").

Nested groups come in two halves. groupOfNames.member already accepts a group
DN, so nesting needs no schema -- what it needs is resolution, which no
released OpenLDAP performs. The all-in-one image therefore builds slapd from a
pinned master commit for the nestgroup overlay, and the app computes the
closure itself when pointed at a server without it. Both paths are covered.

member-values is deliberately left out of nestgroup-flags: it expands `member`
when reading a group, which destroys the distinction between "listed here" and
"reachable through a nested group" and is not recoverable afterwards.

Full suite green in both resolution modes: 215 passed, 2 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 01:22:08 -04:00
wmantly e9b808d1c2 Add cross-app super admin group; rename Executive page to Overview
- app_super_admin is a new cross-app LDAP group (also recognized by proxy
  and jump-host) that grants full admin here regardless of app_sso_admin
  membership: bypassed centrally in utils/permission.js's byGroup, folded
  into GET /api/user/me's isAdmin flag, and added to nav/forceLogin gates
  alongside app_sso_admin.
- Renamed the Executive page to Overview (route, view, API path
  /api/metrics/overview, nav label, docs), keeping /executive as a 301
  redirect alongside the existing /admin, /notifications, /dashboard
  legacy redirects.
2026-07-30 11:56:58 -04:00
wmantly 208762f0d1 Unify the front-end UI shell across the theta42 apps
views/top.ejs, views/bottom.ejs and public/lib/js/app-base.js are now
byte-identical across sso-manager-node, proxy and jump-host. Everything
per-app moved into utils/ui.js, exposed to every render as `ui` via
app.locals (nav items + their group gates, footer repo/docs/ToS links,
favicon, profile/logout targets, update-banner on/off + label).

Client framework changes:
- One gating model everywhere: app-base.js reveals .group-required-<cn>
  for each of the current user user/me groups. sso-manager-node sends LDAP
  DNs in memberOf, the OIDC clients send CNs in groups; both normalise to
  CNs, and the clients isAdmin flag becomes a synthetic `admin` group, so
  proxy nav-admin items are now group-required-admin.
- user/me is fetched once per page load and cached (app.auth.loadUser);
  nav, forceLogin and group-required elements all read that one promise.
- isLoggedIn is dual-mode (Promise + node-style callback), so the async
  and callback call styles both work from one shared top.ejs.
- forceLogin no longer uses $.holdReady (removed in jQuery 4): it redirects
  to /login?redirect=<path>, and still enforces required groups.
- logOut only clears the session; the caller decides where to go next.
- post/put/delete are dual-mode Promise/callback, which also removes the
  undefined `callback2` reference that threw on a non-function callback.

Dependencies: jquery ^4.0.0 and ejs ^3.1.10 in all three apps.

sso-manager-node specifics:
- val.js adopts the shared superset (adds the target/hostname rules and
  the password policy, and fixes the let-shadowed `message` that stopped
  custom rule messages from reaching validateMessage).
- GET /api/user/me now also reports isAdmin (membership in app_sso_admin).
- public/js/app.js: $.isFunction -> typeof (removed in jQuery 4).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 22:57:34 -04:00
wmantly fc0d9104d0 Release 1.4.0: shared @simpleworkjs/* packages; fix discovery envelope drift + client_secret_hash leak
Rewire onto @simpleworkjs/directory-schema, /ldap, and /app-stack. The
directory discovery API now returns the {results} envelope via explicit
/resources, /resources/:slug, /graph, /me handlers and routes every read
through projectResource/projectResources, which unconditionally strips
client_secret_hash (and any /secret|password|privatekey/i key) and reduces
metadata to a public allowlist for non-admins — closing the leak where the ORM
serialized metadata wholesale. The dead routes/api_discovery.js (mounted after
the 404 catcher) is removed; ?group= now returns 200 instead of 404. user_ldap
+ group_ldap take escapeFilter/escapeDN + makeClient/withClient from the shared
ldap package (posix/write-side stays app-local; cert validation unchanged).
build_info unified to {buildVersion,buildHash,buildYear}; ldapts ^8.1.8. New
tests/discovery.test.js locks in the envelope + no-secrets guarantees. Lockfile
regenerated from the registry (no file:/link:).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-25 16:39:10 -04:00
wmantly c4d7a1a8e9 feat: actionable metrics, LDAP log parsing, UI updates 2026-07-22 21:58:06 -04:00
wmantly 4b0a9e9038 Add standalone backup script and admin update-check banner
ops/backup.sh snapshots LDAP (slapcat), Redis (BGSAVE, dynamic RDB path
lookup), and ./config for standalone deployments, with retention. A
background service polls GitHub releases every 24h and surfaces an
admin-only banner in the UI when a newer version is published.
2026-07-15 22:33:55 -04:00
wmantly 38cc6696a4 Fix commit hash not showing in Docker builds (#43)
* Fix commit hash not showing in Docker builds

build_info.js computed buildHash via `git rev-parse --short HEAD` at
runtime, but the final image intentionally has no git binary and no
.git directory (kept lean, per .dockerignore) — so this always failed
silently and the footer's version line showed "unknown" for every
Docker deployment. Working correctly only for bare-metal/dev, where
git + .git are actually present.

Added a throwaway gitinfo build stage that reuses the main base image
(no extra pull) with git installed just for this stage, reads .git
from the build context (now no longer excluded — see .dockerignore),
and bakes the resolved short hash into a small file that IS copied
into the final image. build_info.js reads that file first, falling
back to the old git-rev-parse behavior (still needed for bare-metal).

Verified against a real build: `docker exec sso-manager cat
/app/.build_commit` matches `git rev-parse --short HEAD` on the host,
and the footer now shows the real hash instead of "unknown". Same fix
already applied to proxy (theta42/proxy#133).

* Support GIT_COMMIT build-arg override for submodule builds

The gitinfo stage from the previous commit works for a standalone
clone (.git is a real directory) but not when this repo is built as a
git submodule (e.g. from theta-env): a submodule's .git is a pointer
FILE, not a directory — the real object database lives in the
superproject's .git/modules/, outside this repo's own directory and
therefore outside Docker's build context entirely. `git rev-parse`
can never resolve it from in here no matter what, so builds via
theta-env still baked in "unknown" despite the earlier fix.

Add an optional GIT_COMMIT build-arg that, when set, wins over the
in-context git resolution. theta-env's setup.sh now computes it on the
host (where the submodule DOES resolve correctly) and passes it via
docker-compose.yml's build.args. Same fix in proxy: theta42/proxy#133.

Verified via theta-env's actual setup.sh end to end: rebuilding with
this change, `docker exec sso-manager cat /app/.build_commit` now
matches `git -C sso-manager-node rev-parse --short HEAD` on the host
(previously: "unknown").
2026-07-14 22:32:35 -04:00
wmantly 3ceeeeeca1 Clean up footer: fix copyright, move GitHub link out of the nav (#41)
- Copyright was "© <year> <name>" where <name> is conf.name — an
  operator-configurable display name (e.g. whatever CFG_ORG is set
  to), not a real copyright holder. Changed to "© <year> theta42",
  matching the LICENSE file. Also dropped "All rights reserved",
  which contradicts the MIT license this project ships under; added
  an explicit MIT License link instead.
- Moved the GitHub icon link out of the top nav (where it competed
  with actual navigation items) and into the footer, alongside the
  license link and version/build info.
- Deduplicated the identical buildVersion/buildHash/buildYear
  computation that was copy-pasted in both routes/index.js and
  routes/oauth.js into a shared nodejs/utils/build_info.js.

Verified by rendering top+bottom with the real ejs package: no
template errors, GitHub link present exactly once (in the footer,
not the nav), "All rights reserved" gone, MIT License link present.
npm test failures (155) are pre-existing/environmental (no LDAP
server here) — identical failure count with these changes stashed
out.
2026-07-14 20:54:36 -04:00
wmantly 5644bfa5ec Updated frontend 2026-07-01 11:10:09 -04:00
wmantly 07f3c8b023 Permission based on owners of groups 2020-12-31 15:00:02 -05:00
wmantly b9d2304974 Added function to check group permission. 2020-12-30 14:33:07 -05:00
wmantly 4d51a4ac9e rc1 2020-05-05 23:07:00 -04:00
wmantly 8dc0e946b1 beta 2020-05-03 18:22:51 -04:00