Compare commits

..

12 Commits

Author SHA1 Message Date
wmantly 37f2ece172 Merge pull request #87 from theta42/release-1.1.15
Bump version to 1.1.15
2026-07-18 01:20:13 -04:00
wmantly b77704089b Bump version to 1.1.15; update CHANGELOG
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 01:18:05 -04:00
wmantly 114d8c86ca Merge pull request #86 from theta42/install-script-rework
Rewrite install.sh as a git-clone installer, add a one-line install
2026-07-18 01:17:18 -04:00
wmantly 3e87ad86ab Rewrite install.sh as a git-clone installer, add a one-line install
Replaces the old flag-driven, copy-based installer with an idempotent
git-clone-and-symlink installer matching theta42/proxy's ops/install.sh
pattern, so `wget -O - .../install.sh | sudo bash` works the same way
for both apps:

- Installs to /opt/theta42/sso-manager (was /opt/sso-manager, and the
  repo had to already be checked out locally -- now it clones itself).
- First run only: bootstraps OpenLDAP (modules, overlays, schema,
  directory tree, SSO groups -- ops/ldap-setup.sh) 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.service now points at the new install path
  and sets CONF_SECRETS=/etc/sso-manager/secrets.js (requires
  @simpleworkjs/conf >= 1.2.0, already the pinned version) instead of
  the app needing a config file inside the repo checkout.
- Prints the version it's updating from/to (or "Already up to date")
  on every run, instead of updating silently.

Two real bugs found and fixed while testing this end-to-end in a clean
container:
- The debconf `slapd/domain` value was computed as
  `${LDAP_BASE_DN#dc=}` ("example,dc=com" for "dc=example,dc=com")
  instead of a proper dotted domain -- slapd's postinst hangs
  indefinitely on a malformed domain instead of failing cleanly.
  Fixed to derive it the same way the secrets file already did
  ("example.com").
- ops/ldap-setup.sh's ppolicy-overlay checks used an LDAP substring
  filter, `(olcOverlay=*ppolicy*)`, against an attribute that doesn't
  support substring matching -- it silently matched nothing even when
  the overlay was correctly configured (stored as "{0}ppolicy"),
  so the final verification always reported failure and `set -e`
  aborted the installer after LDAP was set up but before the app was.
  Fixed to filter on `(objectClass=olcOverlayConfig)` and let the
  existing DN-based grep narrow it down, matching the pattern already
  used by every other check in that script.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 01:13:51 -04:00
wmantly c5a2c0a71d Merge pull request #85 from theta42/release-1.1.14
Bump version to 1.1.14
2026-07-17 23:46:43 -04:00
wmantly fe23d231be Bump version to 1.1.14; update CHANGELOG
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 23:44:34 -04:00
wmantly 3a612dbed7 Merge pull request #84 from theta42/bump-conf-jqrepeat
Bump @simpleworkjs/conf to 1.2.0, jq-repeat to 2.2.0
2026-07-17 23:41:19 -04:00
wmantly f154bb8db0 Bump @simpleworkjs/conf to 1.2.0, jq-repeat to 2.2.0
conf 1.2.0 adds CONF_SECRETS, an env var to point at the secrets file
directly -- use it in the Docker entrypoint instead of symlinking the
mounted file into /app/conf/secrets.js, so the app no longer needs
write access to its own conf/ directory to pick up mounted secrets.
jq-repeat 2.2.0 is a compatible feature release (sort(), replace(),
faster leading-edge update() timing); no call-site changes needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 23:38:56 -04:00
wmantly 9fc5abda2a Merge pull request #83 from theta42/fix-changelog-corruption
Fix CHANGELOG.md corruption (v1.1.10 merged into v1.1.11)
2026-07-17 22:22:21 -04:00
wmantly a6ee985de4 Fix CHANGELOG.md: restore separate heading for v1.1.10
A repeated Edit-tool bump had overwritten the previous top version
heading instead of inserting a new one above it, silently merging
v1.1.10's release notes into v1.1.11's section with the v1.1.10
heading missing entirely. The underlying content was still present,
just missing its own "## [1.1.10]" header -- restored.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 22:20:02 -04:00
wmantly 47a9f6c3ec Merge pull request #82 from theta42/fix-doc-link-slugs
Resolve doc cross-links by real filename as a fallback
2026-07-17 22:06:08 -04:00
wmantly f6552cb741 Resolve doc cross-links by real filename as a fallback
The new concept docs (and their "See also" reciprocal links) reference
each other by real filename -- "concepts-accounts.html" -- which is the
correct, working URL on the Jekyll/GitHub Pages build (a page's URL there
IS its filename stem), but doesn't match this viewer's own short slugs
(DOCS keys, e.g. "accounts" -> /docs/accounts), so fixDocLinks() left
those links unrewritten and 404ing in-app.

Rather than rewrite the docs to two different link forms depending on
target, resolve by filename as a fallback when the slug lookup misses --
one link written in a doc now works correctly on both targets.

Bumps to v1.1.13.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 22:03:52 -04:00
11 changed files with 348 additions and 782 deletions
+26 -3
View File
@@ -6,6 +6,26 @@ correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`.
## [Unreleased]
## [1.1.15] - 2026-07-18
### Changed
- Rewrote `install.sh` as an idempotent git-clone installer, replacing the old flag-driven, copy-based one — `wget -O - .../install.sh | sudo bash` now 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.service` sets `CONF_SECRETS=/etc/sso-manager/secrets.js` to match.
- `install.sh` now prints the version it's updating from/to (or "Already up to date") on every run.
### Fixed
- `install.sh` could hang indefinitely on a fresh host if a base package pulled in `tzdata` as a new dependency (no TTY for the interactive timezone prompt), or if the debconf `slapd/domain` value 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/conf` to 1.2.0 and `jq-repeat` to 2.2.0. The Docker entrypoint now sets the new `CONF_SECRETS` env var to point directly at a mounted `sso-secrets.js` instead of symlinking it into `/app/conf/secrets.js` — the app no longer needs write access to its own `conf/` directory to pick up mounted secrets.
## [1.1.13] - 2026-07-17
### Fixed
- The new concept docs' cross-links (`concepts-accounts.html` etc.) 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
@@ -14,12 +34,12 @@ correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`.
### Fixed
- The in-app docs viewer rendered every `docs/*.md` page 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 `.html` suffix — they're now rewritten to the correct in-app URL, the same way image paths already were.
Bumps to v1.1.12.
## [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.
Bumps to v1.1.11.
## [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).
@@ -96,7 +116,10 @@ First tagged release. Establishes the `vX.Y.Z` tag convention that the in-app up
- Unix/POSIX and LDAP bind-only service account support, distinct from real-person accounts.
- Merged OAuth Apps + LDAP Info into a single Integrations page.
[Unreleased]: https://github.com/theta42/sso-manager-node/compare/v1.1.12...HEAD
[Unreleased]: https://github.com/theta42/sso-manager-node/compare/v1.1.15...HEAD
[1.1.15]: https://github.com/theta42/sso-manager-node/compare/v1.1.14...v1.1.15
[1.1.14]: https://github.com/theta42/sso-manager-node/compare/v1.1.13...v1.1.14
[1.1.13]: https://github.com/theta42/sso-manager-node/compare/v1.1.12...v1.1.13
[1.1.12]: https://github.com/theta42/sso-manager-node/compare/v1.1.11...v1.1.12
[1.1.11]: https://github.com/theta42/sso-manager-node/compare/v1.1.10...v1.1.11
[1.1.10]: https://github.com/theta42/sso-manager-node/compare/v1.1.9...v1.1.10
+42 -32
View File
@@ -332,10 +332,17 @@ OAuth clients live in SSO Redis and are preserved by the volume.
## Method 2: Bare metal (Debian/Ubuntu)
`install.sh` is an idempotent installer: it installs Node.js 20.x, installs and
configures OpenLDAP (modules + overlays + custom schema + directory tree +
required groups), deploys the app to `/opt/sso-manager`, and creates a systemd
unit. Configuration is written to `/opt/sso-manager/conf/secrets.js` (file-based).
`install.sh` is an idempotent installer: it installs Node.js 22.x and Redis,
force-syncs the repo to `/opt/theta42/sso-manager`, and symlinks the systemd
config from the repo. Re-run it to update — it prints the version you're
updating from and to (or "Already up to date" if there's nothing new).
On the **first run only** it also installs and configures OpenLDAP (modules +
overlays + custom schema + directory tree + required groups — see
`ops/ldap-setup.sh`) and seeds `/etc/sso-manager/secrets.js` with a generated
LDAP admin password and JWT secret (SMTP is left as a placeholder). Once that
file exists it's never touched again, and LDAP is never re-bootstrapped —
edit the file and restart the service to change anything.
### Prerequisites
@@ -346,47 +353,50 @@ unit. Configuration is written to `/opt/sso-manager/conf/secrets.js` (file-based
### Install
```bash
sudo ./install.sh \
-p 'your-ldap-password' \
-b 'dc=yourdomain,dc=com' \
-n 'Your Org' \
-o 3001
wget -O - https://raw.githubusercontent.com/theta42/sso-manager-node/master/install.sh | sudo bash
```
| Flag | Env var | Description |
|------|---------|-------------|
| `-p, --admin-pass` | `LDAP_ADMIN_PASS` | LDAP admin password (required) |
| `-b, --base-dn` | `LDAP_BASE_DN` | Base DN (default `dc=example,dc=com`) |
| `-n, --org-name` | `ORG_NAME` | Org name (default `SSO Manager`) |
| `-o, --port` | `PORT` | HTTP port (default `3001`) |
| `-j, --jwt-secret` | `JWT_SECRET` | JWT secret (default auto-generated) |
| `-s, --smtp-config` | `SMTP_*` | SMTP as `host:port:user:pass` |
| `--skip-ldap` | `SKIP_LDAP` | Skip LDAP setup (use existing) |
| `--skip-app` | `SKIP_APP` | LDAP setup only |
| `--dry-run` | `DRY_RUN` | Show actions without making changes |
or, if you already have the repo checked out:
```bash
sudo ./install.sh
```
| Env var | Description |
|---------|-------------|
| `LDAP_BASE_DN` | Base DN (default `dc=example,dc=com`) — first run only |
| `LDAP_ADMIN_PASS` | LDAP admin password (default auto-generated) — first run only |
| `JWT_SECRET` | JWT secret (default auto-generated) — first run only |
| `ORG_NAME` | Org name (default `SSO Manager`) — first run only |
| `PORT` | HTTP port (default `3001`) — first run only |
| `SKIP_LDAP` | `true` to skip OpenLDAP bootstrap entirely (point at an existing server yourself) |
| `REPO_URL`, `REPO_DIR`, `BRANCH`, `SECRETS_FILE` | Override the defaults |
### Post-install
```bash
sudo systemctl enable --now sso-manager
sudo systemctl status sso-manager
journalctl -fu sso-manager
curl http://localhost:3001/health # -> {"status":"ok"}
```
### What `install.sh` does
1. Installs Node.js 20.x (NodeSource).
2. Installs OpenLDAP (`slapd`) with: `pw-sha2`, `ppolicy`, `memberof`, `refint`
modules + overlays; the custom `theta42Person` schema (`dateOfBirth`); indexes;
`ou=people`/`ou=groups`/`ou=policies`; a default `pwdPolicy`; and the SSO groups.
3. Installs the app to `/opt/sso-manager` and runs `npm ci --omit=dev`.
4. Generates `conf/secrets.js` (LDAP/SMTP/JWT) and `conf/base.js` (generic defaults).
5. Installs `sso-manager.service` (systemd), enabled on boot.
1. Installs Node.js 22.x (NodeSource) and Redis.
2. Clones/updates the repo at `/opt/theta42/sso-manager`.
3. **First run only:** installs OpenLDAP (`slapd`) with `pw-sha2`, `ppolicy`,
`memberof`, `refint` modules + overlays; the custom `theta42Person` schema
(`dateOfBirth`); indexes; `ou=people`/`ou=groups`/`ou=policies`; a default
`pwdPolicy`; and the SSO groups — then seeds `/etc/sso-manager/secrets.js`.
4. Symlinks `ops/systemd/sso-manager.service` into `/etc/systemd/system` and
runs `npm ci --omit=dev`.
5. Enables and (re)starts the service.
> For an existing LDAP server, run `sudo ./install.sh --skip-ldap …` and point the
> app at it. For LDAP-only setup on a host that already runs the app elsewhere, use
> `--skip-app`. To (re)configure overlays on an already-installed slapd, prefer
> `ops/ldap-setup.sh` (idempotent, auto-detects the user database).
> For an existing LDAP server, run with `SKIP_LDAP=true` and write
> `/etc/sso-manager/secrets.js` yourself (see `secrets.js.example`) before
> starting the service. To (re)configure overlays on an already-installed
> slapd, use `ops/ldap-setup.sh` directly (idempotent, auto-detects the user
> database).
---
+14 -13
View File
@@ -107,23 +107,24 @@ vars, LDAPS/TLS, and backups.
### 3. Bare metal on Debian/Ubuntu
`install.sh` is an idempotent installer: it installs Node.js 20.x and OpenLDAP,
configures the directory (modules, overlays, schema, the SSO groups), deploys
the app to `/opt/sso-manager`, and creates a systemd unit.
The only thing it requires is the LDAP admin password; the domain (base DN)
defaults to `dc=example,dc=com` if you don't pass one:
An automated installer installs Node.js, Redis, and (on first run) OpenLDAP
configuring the directory (modules, overlays, schema, the SSO groups) and
seeding `/etc/sso-manager/secrets.js` with a generated admin password and JWT
secret — then deploys the app to `/opt/theta42/sso-manager` and starts a
systemd service:
```bash
sudo ./install.sh -p 'your-ldap-password' -b 'dc=yourdomain,dc=com'
sudo systemctl enable --now sso-manager
curl http://localhost:3001/health # -> {"status":"ok"}
wget -O - https://raw.githubusercontent.com/theta42/sso-manager-node/master/install.sh | sudo bash
```
Run `sudo ./install.sh -h` for all flags (`-n` org name, `-o` port, `-j` JWT
secret, `-s` SMTP, `--skip-ldap` to use an existing LDAP, `--dry-run`). Re-run
it to update. Full details in [DEPLOYMENT.md](DEPLOYMENT.md) under *Method 2:
Bare metal*.
That's it — LDAP and the app are both live afterward. Edit
`/etc/sso-manager/secrets.js` (org name, SMTP, a non-default base DN, ...) and
restart the service to customize. It's idempotent and safe to re-run —
re-running it updates the app in place (never touching LDAP or the secrets
file again) and prints the version you're updating from and to (e.g. `Updated
v1.1.13 -> v1.1.14`), or `Already up to date` if there's nothing new. Full
details, including env var overrides (`LDAP_BASE_DN`, `SKIP_LDAP`, ...), in
[DEPLOYMENT.md](DEPLOYMENT.md) under *Method 2: Bare metal*.
## Architecture
+13 -12
View File
@@ -6,12 +6,12 @@
# production, run a dedicated LDAP server and point the app at it via app_*
# env vars (or a mounted conf/secrets.js) using the app-only image.
#
# The app reads its configuration from conf/base.js + conf/secrets.js, deep-merged
# by @simpleworkjs/conf, with `app_*` environment variables as the
# highest-precedence override layer. This entrypoint exports those `app_*`
# vars so the app connects to the bundled slapd without any mounted secrets
# file. Any `app_*` var already set in the environment wins (the values below
# are defaults/fallbacks only).
# The app reads its configuration from conf/base.js + a secrets file, deep-merged
# by @simpleworkjs/conf (requires >= 1.2.0, pinned in nodejs/package-lock.json),
# with `app_*` environment variables as the highest-precedence override layer.
# This entrypoint exports those `app_*` vars so the app connects to the bundled
# slapd without any mounted secrets file. Any `app_*` var already set in the
# environment wins (the values below are defaults/fallbacks only).
set -e
@@ -33,14 +33,15 @@ error() { echo "[ERROR] $*" >&2; }
# ── Optional: load operational config from a mounted secrets.js ──────────────
# The unified theta-env stack mounts ./config/sso-secrets.js at /config and
# treats it as the authoritative source for the SSO's config (LDAP base, admin
# password, org name, JWT secret, ...). When present, symlink it into
# /app/conf/secrets.js so @simpleworkjs/conf reads it, and override the
# env-derived operational vars below with the file's values. When absent
# (standalone / env-var deployments) the env vars set above stay in effect and
# the app_* exports further down are emitted as before.
# password, org name, JWT secret, ...). When present, point CONF_SECRETS at it
# so @simpleworkjs/conf reads it directly (no write access to /app/conf
# needed), and override the env-derived operational vars below with the
# file's values. When absent (standalone / env-var deployments) the env vars
# set above stay in effect and the app_* exports further down are emitted as
# before.
SECRETS_JS_MODE=0
if [[ -f /config/sso-secrets.js ]]; then
ln -sf /config/sso-secrets.js /app/conf/secrets.js
export CONF_SECRETS=/config/sso-secrets.js
SECRETS_JS_MODE=1
# Pull the entrypoint's operational vars out of secrets.js in one node call.
# Node emits `KEY<TAB>base64(value)` lines; we decode each with base64 -d and
+213 -702
View File
@@ -1,719 +1,230 @@
#!/usr/bin/env bash
# install.sh - Idempotent standalone installer for Theta42 SSO Manager
# For Debian/Ubuntu systems
#
# This script:
# 1. Installs Node.js 20.x
# 2. Installs and configures OpenLDAP with required schemas/overlays
# 3. Deploys the SSO Manager application
# 4. Sets up systemd services
# Install / update Theta42 SSO Manager on a fresh or existing host.
#
# Usage:
# sudo ./install.sh [OPTIONS]
# This script is idempotent: run it to install, and re-run it to update. It
# installs system dependencies (Node, OpenLDAP, Redis), force-syncs the repo at
# $REPO_DIR to its remote branch, and symlinks the systemd config straight from
# the repo. Because the config is symlinked, an update is just "sync the repo +
# restart" -- the files under /etc/systemd always track the repo.
#
# Options:
# -p, --admin-pass PASSWORD LDAP admin password (required, or set via LDAP_ADMIN_PASS env)
# -b, --base-dn DN Base DN (default: dc=example,dc=com)
# -n, --org-name NAME Organization name shown in UI/email (default: SSO Manager)
# -o, --port PORT HTTP port for SSO Manager (default: 3001)
# -j, --jwt-secret SECRET JWT secret for OAuth (default: auto-generated)
# -s, --smtp-config CONFIG SMTP config as host:port:user:pass
# --skip-ldap Skip LDAP installation (use existing LDAP)
# --skip-app Skip application installation (LDAP setup only)
# --dry-run Show what would be done without making changes
# -h, --help Show this help
# Secrets live at $SECRETS_FILE (/etc/sso-manager/secrets.js by default),
# outside the repo checkout so they survive the hard reset below. FIRST RUN
# ONLY (no $SECRETS_FILE yet): installs and configures OpenLDAP (modules,
# overlays, custom schema, directory tree, required SSO groups -- see
# ops/ldap-setup.sh), generates an LDAP admin password + JWT secret unless
# given via env, and seeds $SECRETS_FILE with those values plus SMTP
# placeholders. Edit that file (SMTP, org name, ...) and re-run this script to
# apply changes -- once it exists it is never touched again, and LDAP is never
# re-bootstrapped.
#
# Environment variables (alternative to flags):
# LDAP_ADMIN_PASS, LDAP_BASE_DN, PORT, JWT_SECRET, SMTP_*
# Intended to be driven by CI/CD with no human writes on prod: the checkout is
# hard-reset to origin/$BRANCH on every run, so the box deterministically
# mirrors the repo (any drift on the box is discarded).
#
# Usage: sudo ./install.sh (override with REPO_URL=, REPO_DIR=, BRANCH=,
# SECRETS_FILE=, LDAP_BASE_DN=, LDAP_ADMIN_PASS=,
# JWT_SECRET=, ORG_NAME=, PORT=, SKIP_LDAP=true)
set -euo pipefail
# Never block on an interactive git credential prompt in CI.
export GIT_TERMINAL_PROMPT=0
# Never block on an interactive debconf prompt (e.g. tzdata, pulled in as a
# dependency of redis-server/slapd on a box that's never configured it).
export DEBIAN_FRONTEND=noninteractive
# ── Defaults ──────────────────────────────────────────────────────────────────
BASE_DN="${LDAP_BASE_DN:-dc=example,dc=com}"
ADMIN_PASS="${LDAP_ADMIN_PASS:-}"
REPO_URL="${REPO_URL:-https://github.com/theta42/sso-manager-node.git}"
REPO_DIR="${REPO_DIR:-/opt/theta42/sso-manager}"
BRANCH="${BRANCH:-master}"
NODE_MAJOR=22
SECRETS_FILE="${SECRETS_FILE:-/etc/sso-manager/secrets.js}"
LDAP_BASE_DN="${LDAP_BASE_DN:-dc=example,dc=com}"
ORG_NAME="${ORG_NAME:-SSO Manager}"
PORT="${PORT:-3001}"
JWT_SECRET="${JWT_SECRET:-}"
SMTP_HOST="${SMTP_HOST:-}"
SMTP_PORT="${SMTP_PORT:-587}"
SMTP_USER="${SMTP_USER:-}"
SMTP_PASS="${SMTP_PASS:-}"
SKIP_LDAP="${SKIP_LDAP:-false}"
SKIP_APP="${SKIP_APP:-false}"
DRY_RUN="${DRY_RUN:-false}"
INSTALL_DIR="/opt/sso-manager"
SYSTEMD_DIR="/etc/systemd/system"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# ── Helper functions ──────────────────────────────────────────────────────────
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
dry_run() { if [[ "$DRY_RUN" == "true" ]]; then echo "[DRY-RUN] $*"; fi; }
usage() {
grep '^#' "$0" | sed 's/^# \{0,1\}//'
exit 0
}
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-p|--admin-pass)
ADMIN_PASS="$2"
shift 2
;;
-b|--base-dn)
BASE_DN="$2"
shift 2
;;
-n|--org-name)
ORG_NAME="$2"
shift 2
;;
-o|--port)
PORT="$2"
shift 2
;;
-j|--jwt-secret)
JWT_SECRET="$2"
shift 2
;;
-s|--smtp-config)
IFS=':' read -r SMTP_HOST SMTP_PORT SMTP_USER SMTP_PASS <<< "$2"
shift 2
;;
--skip-ldap)
SKIP_LDAP="true"
shift
;;
--skip-app)
SKIP_APP="true"
shift
;;
--dry-run)
DRY_RUN="true"
shift
;;
-h|--help)
usage
;;
*)
error "Unknown option: $1"
usage
;;
esac
done
# Validate required parameters
if [[ -z "$ADMIN_PASS" ]]; then
error "LDAP admin password is required (-p or LDAP_ADMIN_PASS env)"
exit 1
if [ "$(id -u)" -ne 0 ]; then
echo "This script must be run as root (try: sudo $0)" >&2
exit 1
fi
# Generate JWT secret if not provided
if [[ -z "$JWT_SECRET" ]]; then
JWT_SECRET=$(openssl rand -hex 32)
info "Generated JWT secret: ${JWT_SECRET:0:8}..."
# Symlink $1 -> $2, replacing whatever is already at $2 (idempotent).
link(){
ln -sfn "$1" "$2"
echo "linked $2 -> $1"
}
# Read the "version" field out of a package.json without depending on Node
# being installed yet (this runs before the Node.js install step below).
pkg_version(){
sed -n 's/^[[:space:]]*"version":[[:space:]]*"\([^"]*\)".*/\1/p' "$1" | head -1
}
# Installed version before this run touches anything, for the upgrade banner
# at the end. Empty on a fresh install (no prior checkout).
CURRENT_VERSION=""
if [ -f "$REPO_DIR/nodejs/package.json" ]; then
CURRENT_VERSION="$(pkg_version "$REPO_DIR/nodejs/package.json")"
fi
# Derive the DNS domain from the base DN (dc=foo,dc=bar -> foo.bar) for email
# sender defaults. Override with LDAP_DOMAIN if set.
if [[ -z "${LDAP_DOMAIN:-}" ]]; then
LDAP_DOMAIN=$(echo "$BASE_DN" | sed 's/^dc=//; s/,dc=/./g')
# FIRST_RUN gates OpenLDAP bootstrap + secrets seeding below -- both only ever
# happen once, the first time this script runs on a host (i.e. before
# $SECRETS_FILE exists). Every later run only updates the code.
FIRST_RUN=0
[ -f "$SECRETS_FILE" ] || FIRST_RUN=1
echo "==> Base packages"
apt-get update
apt-get install -y --no-install-recommends \
build-essential redis-server \
wget gnupg ca-certificates curl git
echo "==> Node.js ${NODE_MAJOR}.x apt source"
install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| gpg --dearmor --yes -o /etc/apt/keyrings/nodesource.gpg
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" \
> /etc/apt/sources.list.d/nodesource.list
echo "==> Install Node.js"
apt-get update
apt-get install -y nodejs
echo "==> Redis"
systemctl enable --now redis-server
echo "==> Repo checkout at ${REPO_DIR} (branch ${BRANCH})"
install -d "$(dirname "$REPO_DIR")"
if [ -d "$REPO_DIR/.git" ]; then
# Force the box to match the remote branch exactly. No human edits configs
# on prod, so discarding local drift is the desired, deterministic behavior.
git -C "$REPO_DIR" fetch --prune origin
git -C "$REPO_DIR" checkout -B "$BRANCH" "origin/$BRANCH"
git -C "$REPO_DIR" reset --hard "origin/$BRANCH"
git -C "$REPO_DIR" clean -fd
else
git clone --branch "$BRANCH" "$REPO_URL" "$REPO_DIR"
fi
# ── System checks ─────────────────────────────────────────────────────────────
check_root() {
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root (sudo)"
exit 1
fi
}
check_os() {
if [[ ! -f /etc/debian_version ]]; then
error "This script is for Debian/Ubuntu systems only"
exit 1
fi
info "Detected $(cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)"
}
# ── Package installation ──────────────────────────────────────────────────────
install_package() {
local pkg="$1"
if dpkg -l | grep -q "^ii $pkg "; then
info "Package $pkg is already installed"
return 0
fi
dry_run "Would install package: $pkg"
[[ "$DRY_RUN" == "true" ]] && return 0
apt-get update -qq
apt-get install -y -qq "$pkg"
info "Installed $pkg"
}
install_nodejs() {
if command -v node &>/dev/null && node --version | grep -q "v20"; then
info "Node.js 20.x is already installed"
return 0
fi
dry_run "Would install Node.js 20.x"
[[ "$DRY_RUN" == "true" ]] && return 0
info "Installing Node.js 20.x..."
# Use NodeSource repository for Node.js 20.x
apt-get update -qq
apt-get install -y -qq curl gnupg ca-certificates
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >/dev/null 2>&1
apt-get install -y -qq nodejs
info "Installed Node.js $(node --version)"
}
# ── OpenLDAP installation and configuration ───────────────────────────────────
install_openldap() {
if command -v slapd &>/dev/null; then
info "OpenLDAP is already installed"
return 0
fi
dry_run "Would install OpenLDAP"
[[ "$DRY_RUN" == "true" ]] && return 0
info "Installing OpenLDAP..."
# Pre-seed debconf for non-interactive installation
debconf-set-selections << EOF
slapd slapd/internal/adminpw string $ADMIN_PASS
slapd slapd/password1 string $ADMIN_PASS
slapd slapd/password2 string $ADMIN_PASS
slapd slapd/domain string ${BASE_DN#dc=}
slapd slapd/backend string MDB
slapd shared/organization string $ORG_NAME
slapd slapd/purge_database boolean true
slapd slapd/move_old_database boolean true
slapd slapd/invalid_config boolean true
EOF
apt-get update -qq
apt-get install -y -qq slapd ldap-utils
# Configure ldap.conf
cat > /etc/ldap/ldap.conf << LDAPCONF
BASE $BASE_DN
URI ldap://localhost
LDAPCONF
# Set proper permissions
chmod 644 /etc/ldap/ldap.conf
info "OpenLDAP installed"
}
configure_openldap() {
info "Configuring OpenLDAP..."
dry_run "Would configure OpenLDAP with base DN: $BASE_DN"
[[ "$DRY_RUN" == "true" ]] && return 0
# Wait for slapd to be ready
for i in {1..10}; do
if ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" "(objectClass=*)" dn >/dev/null 2>&1; then
info "OpenLDAP is ready"
break
fi
sleep 1
done
# Detect the database DN for our suffix
DB_DN=$(ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" \
"(&(objectClass=olcDatabaseConfig)(olcSuffix=${BASE_DN}))" dn 2>/dev/null \
| grep "^dn:" | head -1 | sed 's/^dn: //')
if [[ -z "$DB_DN" ]]; then
# Try to find any database and update its suffix
DB_DN=$(ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" \
"(objectClass=olcDatabaseConfig)" dn 2>/dev/null \
| grep "^dn:" | head -1 | sed 's/^dn: //')
if [[ -n "$DB_DN" ]]; then
info "Updating database suffix to $BASE_DN"
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF
dn: $DB_DN
changetype: modify
replace: olcSuffix
olcSuffix: $BASE_DN
EOF
fi
fi
if [[ -z "$DB_DN" ]]; then
error "Could not detect OpenLDAP database configuration"
return 1
fi
info "Using database: $DB_DN"
# 1. Load pw-sha2 module
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" "(objectClass=olcModuleList)" olcModuleLoad 2>/dev/null | grep -q "pw-sha2"; then
info "Loading pw-sha2 module..."
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: pw-sha2
EOF
else
info "pw-sha2 module already loaded"
fi
# 2. Load ppolicy module
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" "(objectClass=olcModuleList)" olcModuleLoad 2>/dev/null | grep -q "ppolicy"; then
info "Loading ppolicy module..."
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: ppolicy
EOF
else
info "ppolicy module already loaded"
fi
# 3. Load memberof module
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" "(objectClass=olcModuleList)" olcModuleLoad 2>/dev/null | grep -q "memberof"; then
info "Loading memberof module..."
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF
dn: cn=module{1},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: memberof
EOF
else
info "memberof module already loaded"
fi
# 4. Load refint module
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" "(objectClass=olcModuleList)" olcModuleLoad 2>/dev/null | grep -q "refint"; then
info "Loading refint module..."
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF
dn: cn=module{1},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: refint
EOF
else
info "refint module already loaded"
fi
# 5. Add ppolicy overlay
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "$DB_DN" "(olcOverlay=*ppolicy*)" dn 2>/dev/null | grep -qi "ppolicy"; then
info "Adding ppolicy overlay..."
ldapadd -Q -Y EXTERNAL -H ldapi:/// << EOF
dn: olcOverlay=ppolicy,$DB_DN
objectClass: olcOverlayConfig
objectClass: olcPPolicyConfig
olcOverlay: ppolicy
olcPPolicyDefault: cn=ppolicy,ou=policies,$BASE_DN
olcPPolicyUseLockout: TRUE
EOF
else
info "ppolicy overlay already configured"
fi
# 6. Add memberof overlay
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "$DB_DN" "(olcOverlay=*memberof*)" dn 2>/dev/null | grep -qi "memberof"; then
info "Adding memberof overlay..."
ldapadd -Q -Y EXTERNAL -H ldapi:/// << EOF
dn: olcOverlay=memberof,$DB_DN
objectClass: olcConfig
objectClass: olcMemberOf
objectClass: olcOverlayConfig
objectClass: top
olcOverlay: memberof
olcMemberOfDangling: ignore
olcMemberOfRefInt: TRUE
olcMemberOfGroupOC: groupOfNames
olcMemberOfMemberAD: member
olcMemberOfMemberOfAD: memberOf
EOF
else
info "memberof overlay already configured"
fi
# 7. Add refint overlay
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "$DB_DN" "(olcOverlay=*refint*)" dn 2>/dev/null | grep -qi "refint"; then
info "Adding refint overlay..."
ldapadd -Q -Y EXTERNAL -H ldapi:/// << EOF
dn: olcOverlay=refint,$DB_DN
objectClass: olcConfig
objectClass: olcOverlayConfig
objectClass: olcRefintConfig
objectClass: top
olcOverlay: refint
olcRefintAttribute: memberof member manager owner
EOF
else
info "refint overlay already configured"
fi
# 8. Add database indexes
info "Configuring database indexes..."
for index in "mail eq,sub" "uid eq,sub" "cn eq,sub" "member eq" "uidNumber eq" "gidNumber eq"; do
attr=$(echo "$index" | cut -d' ' -f1)
types=$(echo "$index" | cut -d' ' -f2)
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF || true
dn: $DB_DN
changetype: modify
add: olcDbIndex
olcDbIndex: $attr $types
EOF
done
# 9. Load custom theta42 schema
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=schema,cn=config" "(olcObjectClasses=*theta42Person*)" olcObjectClasses 2>/dev/null | grep -q "theta42"; then
info "Loading custom theta42 schema..."
ldapadd -Q -Y EXTERNAL -H ldapi:/// << EOF
dn: cn=theta42,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: theta42
olcAttributeTypes: ( 1.3.6.1.4.1.99999.1.1
NAME 'dateOfBirth'
DESC 'Date of birth in ISO 8601 format YYYY-MM-DD'
EQUALITY caseExactMatch
SUBSTR caseExactSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )
olcObjectClasses: ( 1.3.6.1.4.1.99999.2.1
NAME 'theta42Person'
DESC 'Theta42 SSO extended person attributes'
AUXILIARY
MAY ( dateOfBirth ) )
EOF
else
info "theta42 schema already loaded"
fi
# 10. Create base directory structure
BIND_DN="cn=admin,$BASE_DN"
# Create base DN if it doesn't exist
if ! ldapsearch -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// -b "$BASE_DN" -s base "(objectClass=*)" dn 2>/dev/null | grep -q "dn:"; then
info "Creating base DN structure..."
DC_VALUE="${BASE_DN#dc=}"
DC_VALUE="${DC_VALUE%%,*}"
ldapadd -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// << EOF
dn: $BASE_DN
objectClass: dcObject
objectClass: organization
dc: $DC_VALUE
o: $ORG_NAME
EOF
else
info "Base DN already exists"
fi
# Create OUs
for ou in people groups policies; do
dn="ou=$ou,$BASE_DN"
if ! ldapsearch -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// -b "$dn" -s base "(objectClass=*)" dn 2>/dev/null | grep -q "dn:"; then
info "Creating $ou OU..."
ldapadd -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// << EOF
dn: ou=$ou,$BASE_DN
objectClass: organizationalUnit
ou: $ou
EOF
else
info "OU $ou already exists"
fi
done
# 11. Create default ppolicy
if ! ldapsearch -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// -b "cn=ppolicy,ou=policies,$BASE_DN" -s base "(objectClass=*)" dn 2>/dev/null | grep -q "dn:"; then
info "Creating default ppolicy..."
ldapadd -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// << EOF
dn: cn=ppolicy,ou=policies,$BASE_DN
objectClass: top
objectClass: organizationalRole
objectClass: pwdPolicy
cn: ppolicy
pwdAttribute: 2.5.4.35
pwdLockout: FALSE
pwdMustChange: FALSE
pwdAllowUserChange: TRUE
EOF
else
info "Default ppolicy already exists"
fi
# 12. Create required SSO groups
for group in app_sso_admin app_sso_invite app_sso_oauth_admin; do
dn="cn=$group,ou=groups,$BASE_DN"
if ! ldapsearch -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// -b "$dn" -s base "(objectClass=*)" dn 2>/dev/null | grep -q "dn:"; then
info "Creating group: $group"
ldapadd -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// << EOF
dn: $dn
objectClass: groupOfNames
objectClass: top
cn: $group
description: $ORG_NAME $group group
member: $BIND_DN
EOF
else
info "Group $group already exists"
fi
done
info "OpenLDAP configuration complete"
}
# ── Application installation ──────────────────────────────────────────────────
install_app() {
info "Installing SSO Manager application..."
dry_run "Would install application to $INSTALL_DIR"
[[ "$DRY_RUN" == "true" ]] && return 0
# Create installation directory
mkdir -p "$INSTALL_DIR"
# Copy application files
info "Copying application files..."
cp -r "$SCRIPT_DIR/nodejs/"* "$INSTALL_DIR/"
# Install npm dependencies
info "Installing npm dependencies..."
cd "$INSTALL_DIR"
npm ci --only=production --quiet
# Create secrets configuration
info "Creating application configuration..."
cat > "$INSTALL_DIR/conf/secrets.js" << SECRETEOF
'use strict';
module.exports = {
port: $PORT,
ldap: {
url: 'ldap://localhost',
bindDN: 'cn=admin,$BASE_DN',
bindPassword: '$ADMIN_PASS',
userBase: 'ou=people,$BASE_DN',
groupBase: 'ou=groups,$BASE_DN',
},
smtp: {
host: '${SMTP_HOST:-localhost}',
port: ${SMTP_PORT:-587},
user: '${SMTP_USER:-}',
pass: '${SMTP_PASS:-}',
from: '${ORG_NAME} <noreply@${LDAP_DOMAIN}>',
},
voipms: {
username: '${VOIPMS_USER:-}',
password: '${VOIPMS_PASS:-}',
did: '${VOIPMS_DID:-}',
},
oauth: {
issuer: '',
jwtSecret: '$JWT_SECRET',
token_lifetime: {
access_token: 3600,
refresh_token: 2592000
}
},
};
SECRETEOF
# Create base configuration
cat > "$INSTALL_DIR/conf/base.js" << BASEEOF
'use strict';
module.exports = {
name: "$ORG_NAME",
userModel: 'ldap',
redis: {
prefix: 'sso_manager_'
},
ldap: {
url: 'ldap://localhost',
bindDN: 'cn=admin,$BASE_DN',
bindPassword: '__IN SECRETS FILE__',
userBase: 'ou=people,$BASE_DN',
groupBase: 'ou=groups,$BASE_DN',
userFilter: '(objectClass=posixAccount)',
userNameAttribute: 'uid'
},
oauth: {
issuer: '',
jwtSecret: '__in secrets file__',
token_lifetime: {
access_token: 3600,
refresh_token: 2592000
}
},
smtp: {
host: 'localhost',
port: 587,
secure: false,
from: '$ORG_NAME <noreply@$LDAP_DOMAIN>',
},
};
BASEEOF
# Set ownership
chown -R root:root "$INSTALL_DIR"
chmod -R 755 "$INSTALL_DIR"
info "Application installed to $INSTALL_DIR"
}
# ── Systemd service configuration ─────────────────────────────────────────────
install_systemd() {
info "Installing systemd service..."
dry_run "Would install systemd service"
[[ "$DRY_RUN" == "true" ]] && return 0
cat > "$SYSTEMD_DIR/sso-manager.service" << UNITEOF
[Unit]
Description=Theta42 SSO Manager
Documentation=file://$INSTALL_DIR/README.md
After=network.target slapd.service
Wants=slapd.service
[Service]
Type=simple
User=root
WorkingDirectory=$INSTALL_DIR
ExecStart=/usr/bin/node $INSTALL_DIR/bin/www
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
Environment=NODE_PORT=$PORT
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
UNITEOF
systemctl daemon-reload
systemctl enable sso-manager.service
info "Systemd service installed"
}
# ── Verification ──────────────────────────────────────────────────────────────
verify_installation() {
info "Verifying installation..."
local errors=0
# Check OpenLDAP
if command -v slapd &>/dev/null; then
if systemctl is-active --quiet slapd; then
info "✓ OpenLDAP is running"
else
warn "✗ OpenLDAP is not running"
((errors++))
fi
else
warn "✗ OpenLDAP is not installed"
((errors++))
fi
# Check application
if [[ -d "$INSTALL_DIR" ]]; then
info "✓ Application is installed"
else
warn "✗ Application is not installed"
((errors++))
fi
# Check systemd service
if systemctl is-enabled --quiet sso-manager.service 2>/dev/null; then
info "✓ Systemd service is enabled"
else
warn "✗ Systemd service is not enabled"
((errors++))
fi
if [[ $errors -eq 0 ]]; then
info "Installation verified successfully"
else
warn "Installation completed with $errors issue(s)"
fi
return $errors
}
# ── Main execution ────────────────────────────────────────────────────────────
main() {
echo
echo "=============================================="
echo " Theta42 SSO Manager Installer"
echo "=============================================="
echo
echo "Configuration:"
echo " Base DN: $BASE_DN"
echo " Port: $PORT"
echo " Install dir: $INSTALL_DIR"
echo " Skip LDAP: $SKIP_LDAP"
echo " Skip App: $SKIP_APP"
echo
check_root
check_os
if [[ "$SKIP_LDAP" != "true" ]]; then
echo
info "=== Installing OpenLDAP ==="
install_openldap
configure_openldap
fi
if [[ "$SKIP_APP" != "true" ]]; then
echo
info "=== Installing SSO Manager ==="
install_nodejs
install_app
install_systemd
fi
echo
verify_installation
echo
echo "=============================================="
echo " Installation Complete!"
echo "=============================================="
echo
if [[ "$SKIP_APP" != "true" ]]; then
info "Start the service with: systemctl start sso-manager"
info "View logs with: journalctl -fu sso-manager"
info "Access the UI at: http://localhost:$PORT"
fi
if [[ "$SKIP_LDAP" != "true" ]]; then
echo
info "LDAP Configuration:"
info " Base DN: $BASE_DN"
info " Bind DN: cn=admin,$BASE_DN"
info " Admin pass: (set by you)"
echo
info "Required SSO groups created:"
info " - app_sso_admin"
info " - app_sso_invite"
info " - app_sso_oauth_admin"
fi
echo
}
main
NEW_VERSION="$(pkg_version "$REPO_DIR/nodejs/package.json")"
if [ "$FIRST_RUN" -eq 1 ] && [ "$SKIP_LDAP" != "true" ]; then
echo "==> First run: bootstrapping OpenLDAP (base DN: ${LDAP_BASE_DN})"
LDAP_ADMIN_PASS="${LDAP_ADMIN_PASS:-$(openssl rand -base64 24 | tr -d '=+/')}"
JWT_SECRET="${JWT_SECRET:-$(openssl rand -hex 32)}"
BIND_DN="cn=admin,${LDAP_BASE_DN}"
# slapd/domain wants a dotted DNS domain (e.g. "example.com"), not the raw
# DN -- "dc=foo,dc=bar" -> "foo.bar". A malformed value here (e.g. the raw
# DN with only the leading "dc=" stripped) makes slapd's postinst hang
# indefinitely instead of failing cleanly.
LDAP_DOMAIN="$(echo "$LDAP_BASE_DN" | sed 's/^dc=//; s/,dc=/./g')"
if ! command -v slapd >/dev/null 2>&1; then
debconf-set-selections <<-EOF
slapd slapd/internal/adminpw password ${LDAP_ADMIN_PASS}
slapd slapd/password1 password ${LDAP_ADMIN_PASS}
slapd slapd/password2 password ${LDAP_ADMIN_PASS}
slapd slapd/domain string ${LDAP_DOMAIN}
slapd shared/organization string ${ORG_NAME}
slapd slapd/purge_database boolean true
slapd slapd/move_old_database boolean true
EOF
apt-get install -y slapd ldap-utils
cat > /etc/ldap/ldap.conf <<-EOF
BASE ${LDAP_BASE_DN}
URI ldap://localhost
EOF
systemctl enable --now slapd
else
echo " slapd already installed -- assuming it already serves ${LDAP_BASE_DN}"
fi
echo "==> Directory structure (ou=people, ou=groups)"
for ou in people groups; do
dn="ou=${ou},${LDAP_BASE_DN}"
if ldapsearch -x -D "$BIND_DN" -w "$LDAP_ADMIN_PASS" -H ldap://localhost -b "$dn" -s base "(objectClass=*)" dn 2>/dev/null | grep -q "^dn:"; then
echo " ${dn} already exists"
else
ldapadd -x -D "$BIND_DN" -w "$LDAP_ADMIN_PASS" -H ldap://localhost <<-EOF
dn: ${dn}
objectClass: organizationalUnit
ou: ${ou}
EOF
echo " ${dn} created"
fi
done
echo "==> LDAP modules, overlays, schema, policy, SSO groups"
"$REPO_DIR/ops/ldap-setup.sh" -p "$LDAP_ADMIN_PASS" -b "$LDAP_BASE_DN" -D "$BIND_DN"
echo "==> Seeding ${SECRETS_FILE}"
install -d -m 0750 "$(dirname "$SECRETS_FILE")"
cat > "$SECRETS_FILE" <<-SECRETSEOF
'use strict';
// Generated by install.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ). Edit freely --
// this file is never overwritten by a later run of install.sh.
// LDAP admin password + JWT secret below were auto-generated; SMTP is a
// placeholder (email delivery won't work until you fill it in).
module.exports = {
port: ${PORT},
name: '${ORG_NAME}',
ldap: {
url: 'ldap://localhost',
bindDN: '${BIND_DN}',
bindPassword: '${LDAP_ADMIN_PASS}',
userBase: 'ou=people,${LDAP_BASE_DN}',
groupBase: 'ou=groups,${LDAP_BASE_DN}',
},
smtp: {
host: 'smtp.example.com',
port: 587,
secure: false,
user: 'noreply@${LDAP_DOMAIN}',
pass: 'set-me',
from: '${ORG_NAME} <noreply@${LDAP_DOMAIN}>',
},
oauth: {
issuer: '',
jwtSecret: '${JWT_SECRET}',
token_lifetime: {
access_token: 3600,
refresh_token: 2592000,
},
},
};
SECRETSEOF
chmod 600 "$SECRETS_FILE"
echo " seeded ${SECRETS_FILE} (LDAP + JWT are live; SMTP is a placeholder)"
echo " \$EDITOR ${SECRETS_FILE}"
echo " then re-run this script (or: sudo systemctl restart sso-manager)"
elif [ "$FIRST_RUN" -eq 1 ]; then
echo "==> SKIP_LDAP=true -- not bootstrapping OpenLDAP or seeding ${SECRETS_FILE}"
echo " Write it yourself (see secrets.js.example) before starting sso-manager."
else
echo "==> ${SECRETS_FILE} already exists, leaving LDAP + secrets untouched"
fi
echo "==> Symlink systemd config from the repo"
link "$REPO_DIR/ops/systemd/sso-manager.service" /etc/systemd/system/sso-manager.service
echo "==> Node dependencies"
# Deterministic, production-only install from the lockfile. Falls back to a
# plain install if the lockfile and manifest are out of step.
( cd "$REPO_DIR/nodejs" && { npm ci --omit=dev || npm install --omit=dev; } )
echo "==> Services"
systemctl daemon-reload
systemctl enable --now sso-manager.service
systemctl restart sso-manager.service
echo "==> Done."
if [ -z "$CURRENT_VERSION" ]; then
echo " Installed v${NEW_VERSION}."
elif [ "$CURRENT_VERSION" = "$NEW_VERSION" ]; then
echo " Already up to date (v${NEW_VERSION})."
else
echo " Updated v${CURRENT_VERSION} -> v${NEW_VERSION}."
fi
echo " Update later with: sudo BRANCH=${BRANCH} $0"
+8 -8
View File
@@ -1,12 +1,12 @@
{
"name": "t42-sso-manager",
"version": "1.1.12",
"version": "1.1.15",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "t42-sso-manager",
"version": "1.1.12",
"version": "1.1.15",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-free": "^7.3.0",
@@ -1127,9 +1127,9 @@
}
},
"node_modules/@simpleworkjs/conf": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@simpleworkjs/conf/-/conf-1.1.0.tgz",
"integrity": "sha512-MKRQQ4JAH2tbEm87NdkmfikTT58Tyk/SFbvCC7zKja0bK6j8zYyBXTQUJ0rnvFOVEalDWd/au4AEiptOCEqgvA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@simpleworkjs/conf/-/conf-1.2.0.tgz",
"integrity": "sha512-X4u1oRb0A0x7wzmyiIH5hPYYIFJYUXhYVe9CPX6G6INouRIeZuHlx0pthHlihiAAIc3+KqZBx18qirFN8RoJwA==",
"license": "MIT",
"dependencies": {
"extend": "^3.0.2"
@@ -4357,9 +4357,9 @@
}
},
"node_modules/jq-repeat": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/jq-repeat/-/jq-repeat-2.1.0.tgz",
"integrity": "sha512-e1OmSWeBEHEtyOhNVysx0bnT5wd6HlZ37JZgPcGPmACJ0K9bXDPq0xOwrM1slQMSTw7FOSNDX+MD6VwvPeeZyQ==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/jq-repeat/-/jq-repeat-2.2.0.tgz",
"integrity": "sha512-OdKAQJ8SOTZzoNL/76o5+WJehXnMCoP8aXbDtZCmDh3vuGGdXfN14FkPTqLpZC5xmlv+QVfTXu/UaIRsDjVuhA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "t42-sso-manager",
"version": "1.1.12",
"version": "1.1.15",
"private": true,
"author": [
{
@@ -23,7 +23,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^7.3.0",
"@popperjs/core": "^2.11.8",
"@simpleworkjs/conf": "^1.1.0",
"@simpleworkjs/conf": "^1.2.0",
"bcrypt": "^6.0.0",
"bootstrap": "^5.3.8",
"compression": "^1.8.1",
@@ -31,7 +31,7 @@
"express": "^5.2.1",
"express-rate-limit": "^8.5.2",
"extend": "^3.0.2",
"jq-repeat": "^2.1.0",
"jq-repeat": "^2.2.0",
"jquery": "^3.7.1",
"jsonwebtoken": "^9.0.3",
"ldapts": "^8.1.2",
+13 -1
View File
@@ -60,10 +60,22 @@ function fixImagePaths(html) {
// at /docs/<slug> with no .html suffix. Rewrite known doc links to the
// in-app route, same idea as fixImagePaths() above. Only touches slugs that
// actually exist, so an unrelated "foo.html" link is left alone.
// Docs are also linked by their real filename stem (e.g. "concepts-accounts.html"
// for docs/concepts-accounts.md) -- the correct, working link on the Jekyll/
// GitHub Pages build, where the URL IS the filename stem. That doesn't match
// this viewer's own short slugs (DOCS keys, e.g. "accounts"), so also resolve
// by filename as a fallback -- one link written in a doc works correctly on
// both targets, rather than needing two different link forms.
const slugByFilename = Object.fromEntries(
Object.entries(DOCS).map(([slug, d]) => [path.basename(d.file, '.md'), slug])
);
function fixDocLinks(html) {
return html
.replace(/href="index\.html"/g, 'href="/docs"')
.replace(/href="([a-z0-9-]+)\.html"/g, (match, slug) => DOCS[slug] ? `href="/docs/${slug}"` : match);
.replace(/href="([a-z0-9-]+)\.html"/g, (match, name) => {
const slug = DOCS[name] ? name : slugByFilename[name];
return slug ? `href="/docs/${slug}"` : match;
});
}
// docs/*.md files (not the repo-root README/CHANGELOG/API.md) carry Jekyll
+2 -2
View File
@@ -129,7 +129,7 @@ fi
# ── 3. ppolicy overlay ────────────────────────────────────────────────────────
info "ppolicy overlay"
if config_search -b "$DB_DN" "(olcOverlay=*ppolicy*)" dn | grep -qi "^dn:.*ppolicy"; then
if config_search -b "$DB_DN" "(objectClass=olcOverlayConfig)" dn | grep -qi "^dn:.*ppolicy"; then
skip "ppolicy overlay already configured on ${DB_DN}"
else
config_add "dn: olcOverlay=ppolicy,${DB_DN}
@@ -280,7 +280,7 @@ info "verifying ppolicy is active on ${DB_DN}"
VERIFY_FAILED=0
if config_search -b "$DB_DN" "(olcOverlay=*ppolicy*)" dn | grep -qi "^dn:.*ppolicy"; then
if config_search -b "$DB_DN" "(objectClass=olcOverlayConfig)" dn | grep -qi "^dn:.*ppolicy"; then
ok "ppolicy overlay is attached to the user database"
else
warn "ppolicy overlay is NOT attached to ${DB_DN} — active/inactive toggle will fail"
+7 -3
View File
@@ -1,6 +1,7 @@
[Unit]
Description=SSO NodeJS manager Service
After=network.target
Description=Theta42 SSO Manager
After=network.target slapd.service
Wants=slapd.service
StartLimitIntervalSec=0
[Service]
@@ -8,7 +9,10 @@ Type=simple
Restart=always
RestartSec=1
User=root
ExecStart=/usr/bin/env node /var/www/sso-manager-node/nodejs/bin/www
WorkingDirectory=/opt/theta42/sso-manager/nodejs
Environment="NODE_ENV=production"
Environment="CONF_SECRETS=/etc/sso-manager/secrets.js"
ExecStart=/usr/bin/env node /opt/theta42/sso-manager/nodejs/bin/www
[Install]
WantedBy=multi-user.target
+7 -3
View File
@@ -2,10 +2,14 @@
// Example secrets configuration file (file-based config).
//
// Bare-metal: copy to nodejs/conf/secrets.js and fill in your values.
// Bare-metal: install.sh seeds a filled-in version of this file at
// /etc/sso-manager/secrets.js on first run (LDAP + JWT already live; only
// SMTP is left as a placeholder). Only write this one by hand if you're
// skipping install.sh's LDAP bootstrap (SKIP_LDAP=true) or setting up
// manually.
// Docker / unified stack: place at ./config/sso-secrets.js and bind-mount
// ./config at /config (see docker-compose.yml); docker-entrypoint.sh symlinks
// it into /app/conf/secrets.js so @simpleworkjs/conf reads it.
// ./config at /config (see docker-compose.yml); docker-entrypoint.sh points
// the CONF_SECRETS env var at it so @simpleworkjs/conf reads it.
//
// Values here override conf/base.js and win over <environment>.js. `app_*` env
// vars (if any are set) override this file too — so the Docker stack passes NO