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>
Linux hosts are a primary consumer of the directory — PAM/SSSD login,
LDAP-backed sudo rules (sudoRole), and SSH public keys (openssh-lpk) —
which is exactly what the custom schemas exist for. Describe LDAPS /
StartTLS consumers as "Linux hosts and LDAP-native apps" instead of
"legacy apps" across README, DEPLOYMENT, docs, and the Dockerfile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add conf.ldap.ldapsHost / conf.ldap.ldapsPort so the /integrations page
can advertise an internal-only LDAPS hostname separate from the public
OAuth issuer. This avoids forcing admins to port-forward 636 publicly.
- routes/index.js derives LDAPS URL from ldapsHost/ldapsPort with issuer fallback
- integrations.ejs adds a contextual help panel explaining TLS hostname
validation, the public-issuer default, and recommended internal-DNS /
Docker-internal alternatives
- conf/base.js, secrets.js.example, DEPLOYMENT.md, docs/configuration.md,
and docs/ldap.md document and expose the new options
- Add tests/integrations.test.js for default and custom ldapsHost behavior
- Bump version to 1.1.17
Co-authored-by: Claude <noreply@anthropic.com>
- New docs/concepts-{accounts,oauth-apps,api-tokens}.md -- plain-language
guides aimed at less technical readers, each linking onward to the
existing schema/protocol-level doc for anyone who wants that detail.
Card help links (Users, Groups, OAuth cards, My groups, Members of
<uid>'s group) now point here instead of straight at the technical
docs; the LDAP-protocol-wiring cards (raw connection details for
connecting a 3rd-party app) stay pointed at the technical ldap.md,
since that's genuinely the right depth for that task.
- The "New API Token" card had no help link at all -- added, pointing to
the new API Tokens doc.
- Fixed the in-app docs viewer rendering every docs/*.md page with a
garbled heading + stray <hr> at the top: Jekyll front matter (meant
only for the GitHub Pages build) was never stripped before being
handed to the markdown renderer. Also fixed: cross-doc links
(ldap.html, index.html, etc.) never resolved in-app, since this
viewer serves docs at /docs/<slug> with no .html suffix -- rewritten
to the correct in-app URL, same idea as the existing image-path fix.
Bumps to v1.1.12.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
Every account gets a personal posixGroup at creation (its primary GID
holder) but there was no way to manage its memberUid list -- add
add/remove endpoints and a profile-page UI (admin-only), reusing the
userSelect widget already built for the manager field.
Bumps to v1.1.9.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
Removes the LDAP bind-only service account type in favor of a single
Unix/POSIX account model, surfaced in a new Users > Service Accounts tab.
Adds a multi-valued `manager` field to every account (defaults to the
creator, editable, and grants edit rights on the accounts a person manages
without needing app_sso_admin). homeDirectory and loginShell are now
editable from the profile edit form.
Bumps to v1.1.7.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
The GitHub Pages site used the generic jekyll-theme-cayman theme --
purple gradient hero, no site nav, no per-page SEO. Replaced with a
custom layout that mirrors the actual app UI: dark fixed navbar with
the theta42 logo, Bootstrap 5 + Font Awesome (same stack the app
uses), content in a card, dark footer matching bottom.ejs
(copyright, MIT license, GitHub, Changelog links).
- New cross-page nav (Home/Deployment/Configuration/OAuth/LDAP/
Changelog) -- there was previously no way to get from one docs
page to another except a single "Back to Home" link per page.
- SEO: jekyll-seo-tag + jekyll-sitemap (both GitHub-Pages-supported
plugins, no custom build needed) -- real per-page meta description,
Open Graph/Twitter card tags, canonical URLs, JSON-LD, sitemap.xml,
and a robots.txt referencing it. Added a real description to every
page's front matter (none existed before).
- Mobile: Bootstrap's responsive grid + collapsible navbar; the
screenshot pairs in index.md (inline width="49%" for a two-up
desktop layout) now stack to full-width below 576px instead of
squeezing illegibly small.
Verified with a real Jekyll build (jekyll/jekyll Docker image, no
Ruby available locally) + Playwright: desktop and mobile (375px)
screenshots of the home and deployment pages, mobile nav toggle
open/close, active-link highlighting per page, zero console/page
errors, and confirmed real SEO output (meta description, OG/Twitter
tags, canonical, JSON-LD, sitemap.xml, robots.txt) via curl against
the served site.
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.
The Integrations page's Service Accounts (bind-only, organizationalRole)
don't cover the other real use case: an account something actually runs
as on a Linux host -- a media manager, a torrent client, Emby -- with a
real uidNumber/gidNumber that owns files, and a group other accounts
join for write access (e.g. a `stuff_manager` group granting write
rights to a media library). That needs a real posixAccount, which the
bind-only model can't be.
- New well-known group `app_sso_service_account`, seeded the same way as
app_sso_admin/app_sso_invite/app_sso_oauth_admin (docker-entrypoint.sh,
ops/ldap-setup.sh). Not a permission gate -- a marker.
- "Add new user" form gets a "This is a service account" checkbox: swaps
the person-shaped fields (first/last name, birthday, ToS agreement)
for a single account-name field, since none of those make sense for a
non-person account. On create, the route adds the user to
app_sso_service_account.
- User.listDetail() annotates each user with isServiceAccount (checked
against the marker group's member list once per call, not the memberof
overlay's reverse attribute -- not reliably returned by every LDAP
server this app might point at, confirmed against a real external
directory during testing). Users page shows a "service" badge.
- Notification broadcasts (filter_type=all/all_active) exclude service
accounts by default -- nobody reads mail as `stuff_manager`.
- Fixed a real, previously-unrelated bug this surfaced: addPosixAccount
unconditionally set `mail: data.mail` in the LDAP entry even when
undefined, and ldapts/slapd reject an attribute given an explicit
undefined value ("no values for attribute type") rather than treating
it as absent. This meant creating ANY user without an email already
failed outright -- not something a service account (which commonly has
no real mailbox) could route around. Made mail conditional, matching
how mobile/sshPublicKey/dob already work.
- docs/ldap.md now explains both kinds of service account side by side
and when to use which.
Verified end-to-end against a real external LDAP server (not a local
sandbox): created a service account with no email, confirmed it's
correctly flagged and excluded from broadcast recipient resolution,
confirmed a normal user is unaffected, confirmed the code degrades
gracefully if the marker group doesn't exist yet (pre-upgrade
deployments).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a concrete "Connecting a 3rd-party app or container" section:
a bind-parameter reference table, a worked Gitea example, a generic
Docker LDAP_* env var pattern, and a pointer to theta42/ldap-client for
full host-level (SSH/sudo/PAM) integration as opposed to a single app.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lossless upgrades + config story for the all-in-one image.
Redis persistence (Part A):
- Replace in-memory `--save "" --appendonly no` with AOF + RDB persisted to /data
(--appendonly yes, periodic saves, --dbfilename dump.rdb). OAuth clients,
tokens, and other model-redis state now survive container recreation.
- Add the `sso-data` named volume -> /data in docker-compose.yml.
Config from ./config/sso-secrets.js (Part B):
- docker-entrypoint.sh: when /config/sso-secrets.js is mounted, symlink it to
/app/conf/secrets.js and read the server-side LDAP vars (base DN, admin pass,
org, domain, cert CN, JWT) from the file via one `node` call (base64-decoded,
no eval/quoting hazards). No app_* env is exported in this mode, so the file
is authoritative (@simpleworkjs/conf precedence: base < env < secrets.js <
app_* env). Falls back to the existing LDAP_* env-var mode when the file is
absent (standalone/bare-metal still works).
- docker-compose.yml: trim `environment:` to NODE_ENV/NODE_PORT only and add
`./config:/config:ro`. Removing the app_* env is required — any leftover
app_* would silently override secrets.js.
- secrets.js.example: add orchestrator-only `stack`, `bootstrap`, and
`serviceAccountPass` keys (ignored by the app; read by the entrypoint, the
theta-env bootstrap, and setup.sh).
Backup/restore docs:
- Full "Backups and restore" runbook in DEPLOYMENT.md (what lives where,
manual backup, full / Redis-only / LDAP-only restore, AOF-vs-RDB note,
upgrades). Restore uses slapadd -f (static slapd.conf), and RDB restore
requires deleting the AOF first (AOF wins on startup).
- Pointers in docs/deployment.md and docs/ldap.md; update the Docker Setup
section for the new ./config/ approach (env vars now advanced/optional).
Co-authored-by: Claude <noreply@anthropic.com>