35 KiB
Changelog
All notable changes to this project are documented here. Format loosely
follows Keep a Changelog; versions
correspond to git tags (vX.Y.Z) and nodejs/package.json's version.
[1.8.3] - 2026-07-28
Changed
profile.ejs's self-service API-token UI unified ontoapp.modal, matching the pattern already shipped this round indirectory.ejs, proxy, and jump-host: the static#secretModal/#editModalelements are retired in favor of the sharedapp.modalsingleton, the always-visible inline create-form card becomes a "+ New Token" button + modal, and badge classes switch frombg-*totext-bg-*.- Checkmark-flash copy feedback (silently broken by FontAwesome's
<i>→<svg>replacement) replaced with toast-basedcopyFieldValue, matching proxy and jump-host.
[1.8.2] - 2026-07-28
Fixed
- Creating a new OAuth integration didn't reliably show the "save this client secret now" reveal modal —
saveResource()calledapp.modal.close()immediately before conditionally showing the secret viaapp.modal.open().app.modalis a singleton, andclose()immediately followed byopen()collides with Bootstrap's hide-transition guard. An interveningawait loadResources()made this race unlikely to lose in practice, but not guaranteed to — found while fixing the same, guaranteed-to-lose bug in jump-host and proxy's API-token create flows.
[1.8.1] - 2026-07-28
Fixed
- The resource modal's "Associated LDAP Groups" autocomplete went empty after the first Add/Edit —
loadLdapGroups()'s fetch-once cache guard (if (ldapGroupsCache) return;) also skipped repopulating the<datalist>on every call after the first, but the modal body (including that<datalist>) is rebuilt fresh and empty on everyapp.modal.open(). Now the fetch is still cached, but the datalist is always repopulated.
[1.8.0] - 2026-07-28
Added
- Directory resource modal: General / Details / Associated LDAP Groups / Children tabs, replacing one long form. The new Children tab lists a resource's existing children and lets you add another right from the modal.
- Resource audit trail:
created_by/created_on/updated_by/updated_on, shown in the modal's new footer (mirrors the convention already used by proxy'sHostand jump-host'sApiToken). Existing resources predating this change show "—" until next edited. - Linkable resource URLs:
GET /directory/:slugplus a client-side deep-link check make a resource's modal directly bookmarkable/shareable; the address bar updates to/directory/{slug}while its modal is open and reverts on close (including via the browser Back button). - Auto-created LDAP groups are now prefixed with their nearest ancestor Site's slug (e.g.
site_local_myhost_accessinstead ofmyhost_access), so groups for same-named hosts/services under different sites no longer collide or look identical. Resources with no Site ancestor keep the old unprefixed naming.
Changed
@simpleworkjs/frontendbumped to 0.2.6:app.modalgained thetabs/footer/urloptions (all opt-in, existing callers unaffected) plusshowTab/on/deepLinkSlug/formatAudit/footerButtonshelpers — the shared building blocks behind this release's modal work, reusable by future entity modals in any of the 3 apps.
Fixed
- The Directory's Associated LDAP Groups / Relationships lists no longer risk silently dropping their contents on a second modal open (a
jq-repeat/DOM-rebuild timing race, now rendered manually instead).
Operational note
The new Resource audit fields require a schema migration on any existing deployment: ALTER TABLE Resource ADD COLUMN created_by VARCHAR(255); ALTER TABLE Resource ADD COLUMN created_on INTEGER; ALTER TABLE Resource ADD COLUMN updated_by VARCHAR(255); ALTER TABLE Resource ADD COLUMN updated_on INTEGER; (adjust types for non-sqlite dialects) — @simpleworkjs/orm's sync() only creates missing tables, it never alters existing ones.
[1.7.0] - 2026-07-28
Fixed
formAJAX's loading indicator showed literal HTML ("<div class=..."), not a spinner — it passed raw markup toapp.messages.action, which HTML-escapes its message by design. Replaced with plain text.POST /api/user/(create) andPUT /api/user/passwordhad nomessagefield in their response, so the success notification rendered empty. Added messages matching every other route's convention.- The user landing on
/loginwith a?redirect=had no explanation why — happens whenever another app's "Log in with SSO" bounces an unauthenticated user through/oauth/authorize. Now shows a contextual banner explaining what's happening.
Changed
- Directory: tree view is now the only view (the list/tree toggle is gone) — simpler, one code path.
- Directory: clicking a resource's name opens its detail modal, not just the pencil/edit icon.
Found via a fresh production install's feedback — see the theta-env v1.13.0 release for the full cross-repo summary.
[1.6.3] - 2026-07-28
Fixed
- Group membership changes (
PUT/DELETE /api/group/:group/:uid) didn't invalidate the User cache, soisServiceAccount(and anything else derived frommemberOf) could stay stale for up to 5 minutes after a change. This is what caused a real "lost user" report — the account had landed inapp_sso_service_account(whichusers.ejs's People tab filters out entirely) and looked exactly like data loss, though nothing was ever deleted.
Added
- A confirmation before adding anyone to
app_sso_service_accountvia the Groups page — that group's whole purpose is to hide an account from the People tab, and there was no guardrail against doing that to a real person by mistake (which is how the bug above happened). Every other group's add-member flow is unchanged.
[1.6.2] - 2026-07-28
Fixed
DELETE /api/oauth/client/:id500'd (client.remove is not a function) —OAuthClientwraps@simpleworkjs/orm'sResourcemodel, whose instance delete method is.delete(), not.remove(). The Directory Management UI was unaffected (its own delete routes already used.delete()correctly); only this legacy/raw API endpoint was broken. Found live against a real deployment's SSO API.
Added
- Regression tests: PUT/DELETE on
/api/oauth/client/:idnow verify persistence with a follow-up GET rather than trusting the mutating response alone (this is what would have caught the bug above). A static check across all views/client-side scripts fails CI if any nativealert()/confirm()/prompt()call appears — these block all further browser events on the page and were fully removed in 1.6.1.
[1.6.1] - 2026-07-27
Fixed
- Removed every native
alert()/confirm()call, replacing them withapp.messages.action/confirm/toast. Nativeconfirm()blocks all further browser events on the page (discovered live, mid browser-verification of the 1.6.0app.messages/app.modaladoption, ondirectory.ejs's "Rotate Client Secret" — it froze the whole tab). Also deletedapp.user.remove/app.oauthClient.removeinpublic/js/app.js, which had nativeconfirm()guards and zero callers anywhere in the app.
[1.6.0] - 2026-07-27
Changed
- Adopted
@simpleworkjs/frontend'sapp.messages,app.modal, andapp.validatemodules, replacing the vendoredapp.util.actionMessage/actionConfirm/alertinpublic/lib/js/app-base.jsand the vendoredpublic/lib/js/val.js. Message content is now HTML-escaped (the vendoredalert()this replaces had no escaping), andapp.messages.actionfalls back to a page-wide toast when there's no inline.actionMessagetarget.app.api/app.auth/app.pubsub/app.socketare untouched — they're app-specific (dual-mode callback/promise API,auth-tokenheader injection) and not something the frontend package's genericapp.jsprovides.
[1.5.1] - 2026-07-27
Fixed
PUT /api/user/:uid500'd withObjectClassViolationError(LDAP0x41) when settingsshPublicKeyon any account created before theldapPublicKeyauxiliary objectClass was added to new-user creation (e.g. the bootstrapadminaccount).User.update'ssshPublicKeyhandling andUser.addSSHkey(nodejs/models/user_ldap.js) now add theldapPublicKeyobjectClass first (ignoringTypeOrValueExistsErrorif already present), the same pattern already used fordateOfBirth/theta42Person.- OAuth Integration parent dropdown was blank.
populateHostDropdowninnodejs/views/directory.ejsonly built options forkind === 'host'andkind === 'service'— there was no branch forkind === 'oauth', so choosing "OAuth Integration" in the Directory's add-resource modal left the parent-Service picker empty except the placeholder. Added the missing branch.
[1.5.0] - 2026-07-26
Changed
- Unified the front-end UI shell across the three theta42 apps.
views/top.ejs,views/bottom.ejsandpublic/lib/js/app-base.jsare now byte-identical in sso-manager-node, proxy and jump-host, so the apps look and behave the same and a shell change lands in one edit per repo instead of three divergent ones. Everything that differs between the apps moved into a newnodejs/utils/ui.js, exposed to every render asuiviaapp.locals: nav items and the groups that may see them, footer repo/license/docs/Terms links, favicon, the profile and post-logout targets, and whether the update banner exists at all. - One nav-gating model everywhere.
app-base.jsreveals.group-required-<cn>elements for each group the current user is in, read fromGET /api/user/me. sso-manager-node reports LDAP DNs inmemberOfand the OIDC clients report CNs ingroups; both normalise to CNs client-side, and the clients' effective-rightsisAdminflag is exposed as a syntheticadmingroup — so one gating model covers a group-based provider and boolean-admin clients without either app learning the other's response shape. GET /api/user/meis fetched once per page load and cached (app.auth.loadUser). The nav, per-viewforceLoginand every group-gated element read that one promise instead of issuing their own request.app.auth.isLoggedInis dual-mode: it returns a Promise and invokes an optional node-style callback, so the async and callback call styles both work against one sharedtop.ejs.app.auth.forceLoginno longer uses$.holdReady(removed in jQuery 4). An unauthenticated user is redirected to/login?redirect=<path>; group requirements are still enforced, andlogOutnow only clears the session, leaving the destination to the caller (ui.logoutRedirect).- Dependency alignment across all three apps:
jquery^4.0.0andejs^3.1.10.
Fixed
app.api.deletedropped its callback when called byformAJAX.formAJAXalways passes the serialized form as the second argument, so a DELETE-method form's callback landed in the data slot and never ran.deletenow accepts both(url, callback)and(url, data, callback).app.api.post/putreferenced an undefinedcallback2and threw when handed a non-function callback. Both are now dual-mode Promise/callback.- The login page's "reveal the card once we know you're logged out" branch threw (
Cannot read properties of null) whenever the logged-in check answered before the parser reached that element — which it always did without a stored token. It now runs on DOM ready. logInRedirecton the legacy/login/<path>form kept only the path. The OIDC provider routes an unauthenticated authorization request through/login/oauth/authorize?client_id=…&state=…; dropping the query there loses the entire authorization request. The suffix form now preserves its query string.
Fixed (sso-manager-node)
public/lib/js/val.jsshadowedmessagewithletinsidevalidateField, so a custom rule's return value never reachedvalidateMessageand the caller always saw the generic length message. Resolved by adopting the shared validator, which also brings thetarget/hostnamerules and the real password policy (>= 8 chars, and either 12+ or 3 of 4 character classes) to this app.public/js/app.jsused$.isFunction, removed in jQuery 4.
Added (sso-manager-node)
GET /api/user/menow also reportsisAdmin(membership inapp_sso_admin), the single effective-rights flag the shared UI shell gates the update banner on. Group-level gating still readsmemberOf.
Verified
- Browser-verified against a full theta-env stack (sso-manager + proxy + jump-host): every top-level page renders with a clean console; nav gating is correct for admin and non-admin;
forceLogin's onboarding and group gates fire;val.jsblocks a weak password and accepts a strong one through a real form submit; the DELETE-method forms work; and the OIDC login round trip (authorize with PKCE -> login -> consent -> callback -> token fragment) completes on both OIDC clients.
[1.4.0] - 2026-07-25
Security
- The directory discovery API leaked OAuth
client_secret_hash(and any secret-ish metadata key) to every authenticated caller.Resourcedoesn't overridetoJSON, so the ORM serializedmetadatawholesale — including theclient_secret_hashstored onkind:'oauth'resources — acrossGET /api/discovery/resources,/graph,/me,/resources/:slug, and the directory-adminGET /api/directory-admin/resources. Every discovery read endpoint and the admin list now route throughprojectResource/projectResourcesfrom@simpleworkjs/directory-schema, which unconditionally strips secret keys (anything matching/secret|password|privatekey/i, includingclient_secret_hash) and, for non-directory-admins, reduces metadata to a public allowlist. Admins never receiveclient_secret_hasheither.
Fixed
- Directory discovery envelope drift.
routes/discovery.js(theautoRouter(Resource)mounted live atapp.js:87) returned bare arrays, not the{ results: [...] }envelope the directory contract specifies — so jump-host'sdata.results || []collapsed every per-group query to[]and no user could bridge. Discovery is now served by explicit/resources,/resources/:slug,/graph,/mehandlers that all return the{ results }envelope. The deadroutes/api_discovery.js(mounted atapp.js:112, after the 404 catcher) and its mount were removed. GET /api/discovery/resources?group=<cn>now returns 200 with{ results: [...] }instead of 404 (the autoRouter'ssearchsupported?group=, but the route was effectively unreachable for jump-host's call pattern).
Added
- Adopted the shared
@simpleworkjs/*packages published under the simpleworkjs org:@simpleworkjs/directory-schema— the directory contract: thekindenum,Resource/ResourceEdge/ResourceGroupfield defs, the{ results }envelope, the security projection (projectResource/projectResources/isDirectoryAdmin), and the discovery client.models/resource.jsimports the field defs; the discovery + directory-admin routes use the projection.@simpleworkjs/ldap—models/user_ldap.jsandmodels/group_ldap.jsnow takeescapeFilter/escapeDNandmakeClient/withClientfrom the shared package (via local wrappers that passconf); sso keeps its richUser.get/Group.get/User.login/User.addSSHkey(posix/write-side stays app-local). sso'smakeClientpasses notlsOptions, so cert validation is unchanged.@simpleworkjs/app-stack— unifiedbuild_info({buildVersion, buildHash, buildYear}) and thestatic-modulesmounting helper.utils/build_info.jsand the static-modules loop inroutes/index.jsuse the shared helpers.
- New
tests/discovery.test.js(jest + supertest, runs under the docker harness): locks in the{ results }envelope on/resources,/graph,/me,/resources/:slug, the?group=200-regression, and the no-client_secret_hash/no-secret-key guarantee for every caller.
Changed
- Dependency alignment:
ldapts^8.1.2→^8.1.8. The new@simpleworkjs/*deps resolve from the npm registry (^1.0.0); nofile:/link:entries in the lockfile, sonpm ciis clean in docker builds. build_infoexport shape changed from{commit, version}to{buildVersion, buildHash, buildYear}(the shared shape used by all three apps).
[1.3.2] - 2026-07-23
Fixed
- OAuth client management API returned
client_id: undefinedon every GET. The ORM'sModel.toJSON()only serializes schema fields, so the mappedclient_id/scopes/redirect_uris/… thatOAuthClient.get()attaches to the wrapped Resource were stripped fromGET /api/oauth/clientandGET /api/oauth/client/:idresponses. The theta-env bootstrap (which lists clients and rotates by the returnedclient_id) then called/api/oauth/client/undefined/rotateand got a 500, aborting stack bring-up whenproxy-secrets.jshad no usable secret.OAuthClient.get()now emits an explicit public JSON shape (and deliberately omitsclient_secret_hash, so the secret hash no longer leaks over the API). OAuthClient.get()no longer 500s on an unknown/undefinedclient id:Resource.get()returnsnull(it doesn't throw), which was dereferenced asr.kind. It now returns a clean 404.
[1.3.1] - 2026-07-23
Added
- The Directory documentation (
docs/directory.md) is now surfaced: registered in-app at/docs/directory("Directory & Inventory"), help-linked from the Directory page header, and linked from the docs-site index. Extended with the shared slug conventions (site_<name>,host_<hostname>— as used by ldap-client and the theta-env seed), the automatic-registration story (theta-env stack seeding, ldap-client Linux host enrollment), and the API surface (admin at/api/directory-admin, read-only graph at/api/discovery).
Changed
- Direct LDAP binds are described as first-class, not "legacy", across README, DEPLOYMENT.md, docs, and the Dockerfile: Linux hosts are a primary consumer of the directory (PAM/SSSD login, LDAP-backed
sudoviasudoRole, SSH public keys via openssh-lpk) — exactly what the custom schemas exist for.
[1.3.0] - 2026-07-23
Added
- OAuth client management API at
/api/oauth/client(groupapp_sso_oauth_admin): list, create, update, delete, and rotate-secret for OAuth clients, backed by the Resource model. Accepts form-style string inputs (newline-separatedredirect_uris/allowed_groups, space-separatedscopes). - Dockerized test suite:
docker-compose -f docker-compose.test.yml up --buildspins up OpenLDAP + Redis + a test-runner that seeds the test user and runs the full jest suite (174 tests) against them.tests/globalSetup.jshonorsREDIS_URL.
Fixed
- Completed the model-redis →
@simpleworkjs/ormport that shipped half-finished in 1.2.1:OtpToken.issue/verifycalled nonexistentfind()/listDetail()— every OTP login 500'd.- Impersonation create/revoke called nonexistent
ImpersonationToken.listDetail()— both endpoints 500'd. OAuthClientreadis_validfrom the Resource model, which has no such column — every client evaluated as disabled and all/oauth/authorizerequests were rejected with 400. Client validity now lives inmetadata(absent = valid).OAuthClient.adddidn't set the required-uniqueResource.slug; clients now get a slug derived from the client name.GET /api/token/:name/:tokenreturned{results: null}with 200 for unknown tokens (ormget()returns null instead of throwing); now 404s.
User.loginreturns a clean 401 instead of crashing when neitheruidnorusernameis supplied.- Depend on published
@simpleworkjs/orm^0.2.8 andmodel-redis^1.6.0 instead of a localfile:link that brokenpm ciin docker builds.
Changed
- Removed the Mobile Phone field from the user create/edit form.
[1.2.1] - 2026-07-22
Added
- Actionable Metrics: New real-time metrics tracking for failed logins, top IPs, and service usage per user.
- LDAP Monitor: Background service to parse OpenLDAP binds over port 389 and track metrics for legacy apps.
- UI Updates: Executive dashboard now displays actionable metrics cards instead of raw logs. User profiles show individual service usage stats to admins.
- Directory Management: Integrated site/host/service abstractions into directory UI and allowed associating OAuth apps directly to services.
[1.1.18] - 2026-07-21
Added
- N-Way Multi-Master LDAP replication:
LDAP_SERVER_ID+LDAP_REPLICATION_HOSTSconfiguresyncreplpeers in the bundled OpenLDAP, and a new/sitespage (nav: Sites) shows each configured peer's LDAP URL and live reachability. - A
locationproperty on users, editable from the profile and user-edit forms.
Fixed
/sites(added above) 500'd on every load:views/sites.ejsincluded nonexistent partialsheader/footerinstead of this app's actualtop/bottom. Fixed to match every other view.
Changed
- Refreshed all README screenshots (dashboard, users, groups, OAuth apps) against the current UI, and added a new Sites & Replication screenshot.
[1.1.17] - 2026-07-18
Added
conf.ldap.ldapsHostandconf.ldap.ldapsPortconfig options (also settable viaapp_ldap__ldapsHost/app_ldap__ldapsPort). WhenldapsHostis set, the/integrationspage advertises that hostname for direct LDAPS binds instead of deriving it from the public OAuth issuer. This lets operators use an internal-only hostname (e.g.ldap.internal.example.comorsso-manageron the Docker network) and avoid port-forwarding 636 to the internet.- A contextual help panel on
/integrations→ LDAP explaining why LDAPS needs a hostname (not an IP), why 636 should not be publicly forwarded, and the recommended internal-DNS / Docker-internal alternatives.
Changed
routes/index.jsnow computes the displayed LDAPS URL fromconf.ldap.ldapsHost/ldapsPortwith fallback to the OAuth issuer host for backward compatibility.secrets.js.example,docs/configuration.md,docs/ldap.md, andDEPLOYMENT.mddocument the newldapsHost/ldapsPortoptions and recommended network layouts.- Bumped version to
1.1.17innodejs/package.json.
[1.1.16] - 2026-07-18
Security
- Hardened LDAP filter and DN construction against injection. All user-supplied values interpolated into group filters (
models/group_ldap.js) and RDN values used when adding users/groups (models/user_ldap.js) are now escaped before being sent to the LDAP server. - Replaced
Math.random()-based token generation inmodels/token.js,models/oauth_code.js, andmodels/oauth_client.jswithcrypto.randomUUID()for session tokens, OAuth codes, access/refresh tokens, and client IDs. - Replaced
Math.random()-based OTP generation inOtpToken.issue()withcrypto.randomInt(). routes/oauth.jsnow refuses to start ifoauth.jwtSecretis missing or still set to the placeholder value, instead of falling back to a hardcoded public string.- Rendered docs and Terms-of-Service HTML in
routes/docs.jsandroutes/index.jsare now sanitized withxssto prevent stored XSS from malicious markdown. - Removed a
console.logthat wrote new-user data (including password hashes) to the log inmodels/user_ldap.js; reduced login-path error logging toerror.name/error.messageonly.
Changed
- Public-release packaging: removed
"private": truefromnodejs/package.jsonand bumped version to1.1.16. - CI workflow (
.github/workflows/pr-tests.yml) now setsapp_oauth__jwtSecretso the test suite can run against the new startup-time JWT validation.
Fixed
models/email.js: fixed a template bug where the renderedfromaddress usedtemplate.messageinstead oftemplate.from.
1.1.15 - 2026-07-18
Changed
- Rewrote
install.shas an idempotent git-clone installer, replacing the old flag-driven, copy-based one —wget -O - .../install.sh | sudo bashnow works the same way it does for theta42/proxy. Installs to/opt/theta42/sso-manager(was/opt/sso-manager). First run only: bootstraps OpenLDAP with a generated admin password + JWT secret and seeds/etc/sso-manager/secrets.js(was/opt/sso-manager/conf/secrets.js, hand-filled from CLI flags); later runs never touch LDAP or the secrets file again.ops/systemd/sso-manager.servicesetsCONF_SECRETS=/etc/sso-manager/secrets.jsto match. install.shnow prints the version it's updating from/to (or "Already up to date") on every run.
Fixed
install.shcould hang indefinitely on a fresh host if a base package pulled intzdataas a new dependency (no TTY for the interactive timezone prompt), or if the debconfslapd/domainvalue was malformed (a raw DN fragment instead of a dotted domain) — slapd's postinst hangs rather than failing cleanly on a bad domain. Both fixed.ops/ldap-setup.sh's ppolicy-overlay checks used an LDAP substring filter against an attribute that doesn't support substring matching, so they always reported the overlay as unconfigured even when it was correctly set up (stored as{0}ppolicy) — the final verification step always failed as a result. Fixed to filter on(objectClass=olcOverlayConfig)instead, matching every other check in that script.
1.1.14 - 2026-07-17
Changed
- Bumped
@simpleworkjs/confto 1.2.0 andjq-repeatto 2.2.0. The Docker entrypoint now sets the newCONF_SECRETSenv var to point directly at a mountedsso-secrets.jsinstead of symlinking it into/app/conf/secrets.js— the app no longer needs write access to its ownconf/directory to pick up mounted secrets.
1.1.13 - 2026-07-17
Fixed
- The new concept docs' cross-links (
concepts-accounts.htmletc.) are the correct, working URL on the Jekyll/GitHub Pages build (where the page's URL is its filename stem) but didn't resolve in the in-app docs viewer, which serves docs at a separate short slug (/docs/accounts). The in-app renderer now also resolves a doc's real filename as a fallback, so one link written in a doc works on both targets.
1.1.12 - 2026-07-17
Added
- Three new plain-language docs aimed at less technical readers, replacing the schema-level LDAP/OAuth/API docs as the target of most card help links: Accounts, Groups & Managers, Connecting Apps (SSO), and API Tokens. Each links onward to the deeper technical reference for readers who want it; the technical docs link back the other way too. The personal-access-token card (previously missed) now links to its own doc.
Fixed
- The in-app docs viewer rendered every
docs/*.mdpage with a garbled heading and a stray horizontal rule 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.htmlsuffix — they're now rewritten to the correct in-app URL, the same way image paths already were.
1.1.11 - 2026-07-17
Changed
- Moved the help (❓) link out of the global header and onto each relevant card individually (Invite User, Add new user, User List, Service Accounts, group cards, OAuth/LDAP integration cards, My groups, Members of
<uid>'s group, New API Token) — each now deep-links straight to the doc that actually covers it, instead of one generic header icon.
1.1.10 - 2026-07-17
Added
- A help icon (❓) in the top-right header now deep-links to the doc most relevant to the current page (falls back to the docs index elsewhere).
- The in-app docs viewer (
/docs) is now searchable — a simple line-substring search over the same local doc set, no new dependency, still works with no internet access.
1.1.9 - 2026-07-17
Added
- Every account's personal Unix group (its primary GID holder) can now have supplementary members managed from the account's profile page ("Members of
<uid>'s group", admin-only) — e.g. to share write access to files owned by that group. Uses the standardmemberUidattribute (RFC 2307posixGroup).
1.1.8 - 2026-07-17
Added
- Group membership is now editable directly from a user's profile page ("My groups" -- add via a group-name picker, remove with a button per row), instead of only from each group's own card on the Groups page. Admin-only, using the existing per-group member add/remove endpoints.
Fixed
- The Edit Profile form's Mobile Phone field had a stray
validate=":9"making it effectively required (submission was blocked with "Please fix the form errors" if left blank) -- it was always meant to be optional, matching the "Add user" form. Removed. - A service account's profile always showed
Name: Service Account-- every service account has the same literal filler given/last name (a schema-satisfying placeholder, not meant to be shown), making them indistinguishable by name. The Name line is now hidden for service accounts. - The Users page's Service Accounts tab, and a freshly-created service account's own profile, could appear empty/not-a-service-account for up to 5 minutes right after creation. Creating a user caches it via
User.get()before the route handler marks it as a service account (group membership), so the cached copy hadisServiceAccountstuck wrong until the cache TTL expired. Now cleared and re-fetched immediately after marking. - A user belonging to exactly one LDAP group had their
memberOfattribute returned as a bare string instead of a one-element array (ldapts's normal behavior for single-valued attributes) -- client-side permission checks (for(let group of user.memberOf)) would then iterate the DN character-by-character instead of once, causing pages gated on that group (e.g. Groups) to incorrectly show "You do not have permission to be here." NormalizedmemberOfto always be an array, same fix already applied tomanager.
1.1.7 - 2026-07-17
Changed
- Service accounts unified to one kind. Removed the LDAP bind-only service account type (the Integrations → LDAP "Service Accounts" card, and its
/api/service-accountroutes) -- every service account is now a real Unix/POSIX account with a UID, created from the new Users → Service Accounts tab. Email and password are both optional for service accounts; a blank password means nouserPasswordis set at all (the account simply can't bind). - Added a
managerfield to every account. Multi-valued (a list of usernames), defaults to whoever created the account (the admin who added it, or whoever sent the invite), and reassignable from the account's Edit form. Anyone listed as a manager can edit that account -- same fields an admin can (mobile, description, SSH key, date of birth, home directory, login shell, manager list) -- without needingapp_sso_admin. homeDirectoryandloginShellare now editable from the Edit Profile form (previously view-only).
1.1.6 - 2026-07-16
Changed
- Redesigned the GitHub Pages docs site to match the app's own look (dark navbar/footer, Bootstrap 5, Font Awesome) instead of the generic
jekyll-theme-caymantheme, added a real cross-page nav, SEO (jekyll-seo-tag+jekyll-sitemap, per-page descriptions, OG/Twitter tags, sitemap.xml, robots.txt), and mobile-responsive layout.
1.1.5 - 2026-07-16
Fixed
- Bumped
jq-repeat2.0.1 -> 2.1.0.update()is now trailing-edge throttled (~50ms) even on the first call;profile.ejs's edit-profile flow updated a scope and immediately slid the same element into view, which could briefly show stale/empty data. Deferred the slide by 60ms.
1.1.4 - 2026-07-16
Added
- CI: GitHub Actions now builds the real bundled image, seeds LDAP fixtures, and runs the full Jest suite on every PR (Node 18/20/22) -- this repo had unit tests but nothing ran them automatically until now.
- White-label:
<title>, the navbar brand text, and the favicon were hardcoded "SSO - Theta 42"/"SSO Manager" despiteconf.namealready existing (it was never actually rendered). Newconf.logokey added alongside it. Footer attribution is left as-is. Closes #6.
Fixed
- The bundled default ppolicy entry set
pwdLockout: FALSE, silently making the admin "deactivate user" action not actually block that user's login. Fixed toTRUE, with a drift-correction path inops/ldap-setup.shfor already-deployed instances. A separate, deeper ppolicy-overlay issue remains open as #68. top.ejsreferenced a/static/favicon.svgthat didn't exist inpublic/(a pre-existing 404) -- now uses the existing logo file viaconf.logo.
1.1.3 - 2026-07-16
Added
CHANGELOG.md(this file), backfilled from the release notes for every tag so far and served in-app at/docs/changelog. Closes theta-env#43.
1.1.2 - 2026-07-16
Fixed
- Removed a dead IE<9-only
html5shimscript tag pointing at a domain that no longer resolves.
Added
- In-app documentation:
GET /docsandGET /docs/:slugrender this project's own README, DEPLOYMENT, API.md,docs/{ldap,oauth,configuration}.md, anddirectory_spec.mdserver-side — readable from the running app with no dependency on GitHub Pages, which requires internet access to view. Public, no auth, rate-limited.
1.1.1 - 2026-07-16
Added
- Terms of Service is now editable at runtime by admins.
tos.mdused to be baked into the repo and read once at startup, requiring a code change and deploy to update. It's now a Redis-backed singleton, editable from a new "Terms of Service" card on the admin Dashboard, with the bundledtos.mdused only as a one-time seed for new deployments. Admins can optionally require all users to re-accept the terms after a substantive edit. Closes #39. (#62)
1.1.0 - 2026-07-16
First tagged release. Establishes the vX.Y.Z tag convention that the in-app update-check banner polls against going forward.
Added
- Standalone backup script (
ops/backup.sh) — snapshots LDAP (slapcat), Redis, and./config, with retention. - Admin-only in-app banner that checks GitHub releases every 24h and surfaces available updates.
- Unix/POSIX and LDAP bind-only service account support, distinct from real-person accounts.
- Merged OAuth Apps + LDAP Info into a single Integrations page.