Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a8030fd7d | |||
| cf80c966eb | |||
| 07819a6254 | |||
| 1b3e842006 | |||
| efe3e514b0 | |||
| 37f2ece172 | |||
| b77704089b | |||
| 114d8c86ca | |||
| 3e87ad86ab | |||
| c5a2c0a71d | |||
| fe23d231be | |||
| 3a612dbed7 | |||
| f154bb8db0 | |||
| 9fc5abda2a | |||
| a6ee985de4 | |||
| 47a9f6c3ec | |||
| f6552cb741 |
@@ -136,6 +136,10 @@ jobs:
|
||||
# directory layout (dc=example,dc=com) -- only the admin password
|
||||
# (normally supplied via a gitignored secrets.js) needs setting.
|
||||
app_ldap__bindPassword: your-ldap-password
|
||||
# routes/oauth.js now refuses to start without a real jwtSecret.
|
||||
# This is a non-secret test value; the container under test uses
|
||||
# secrets.js.example's jwtSecret independently.
|
||||
app_oauth__jwtSecret: ci-test-jwt-secret-do-not-use-in-production
|
||||
run: npm test
|
||||
|
||||
test-summary:
|
||||
|
||||
+43
-3
@@ -6,6 +6,43 @@ correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [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 in `models/token.js`, `models/oauth_code.js`, and `models/oauth_client.js` with `crypto.randomUUID()` for session tokens, OAuth codes, access/refresh tokens, and client IDs.
|
||||
- Replaced `Math.random()`-based OTP generation in `OtpToken.issue()` with `crypto.randomInt()`.
|
||||
- `routes/oauth.js` now refuses to start if `oauth.jwtSecret` is 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.js` and `routes/index.js` are now sanitized with `xss` to prevent stored XSS from malicious markdown.
|
||||
- Removed a `console.log` that wrote new-user data (including password hashes) to the log in `models/user_ldap.js`; reduced login-path error logging to `error.name`/`error.message` only.
|
||||
|
||||
### Changed
|
||||
- Public-release packaging: removed `"private": true` from `nodejs/package.json` and bumped version to `1.1.16`.
|
||||
- CI workflow (`.github/workflows/pr-tests.yml`) now sets `app_oauth__jwtSecret` so the test suite can run against the new startup-time JWT validation.
|
||||
|
||||
### Fixed
|
||||
- `models/email.js`: fixed a template bug where the rendered `from` address used `template.message` instead of `template.from`.
|
||||
|
||||
## [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 +51,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 +133,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.16...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
@@ -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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -23,7 +23,7 @@ Auth.login = async function(data){
|
||||
|
||||
return {user, token}
|
||||
}catch(error){
|
||||
console.error("AUTH LOGIN error:", error);
|
||||
console.error("AUTH LOGIN error:", error.name, error.message);
|
||||
throw this.errors.login();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -58,7 +58,7 @@ Mail.sendTemplate = async function(to, template, context, from){
|
||||
to,
|
||||
mustache.render(template.subject, context),
|
||||
mustache.render(template.message, context),
|
||||
from || (template.from && mustache.render(template.message, context))
|
||||
from || (template.from && mustache.render(template.from, context))
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,31 @@ const { Client, Attribute, Change } = require('ldapts');
|
||||
const { LRUCache } = require('lru-cache');
|
||||
const conf = require('@simpleworkjs/conf').ldap;
|
||||
|
||||
// Escape a value used inside an LDAP search filter (RFC 4515).
|
||||
function escapeLDAPSearchValue(val) {
|
||||
return String(val)
|
||||
.replace(/\\/g, '\\5c')
|
||||
.replace(/\*/g, '\\2a')
|
||||
.replace(/\(/g, '\\28')
|
||||
.replace(/\)/g, '\\29')
|
||||
.replace(/\0/g, '\\00');
|
||||
}
|
||||
|
||||
// Escape a value used in an LDAP DN (RFC 4514). Defensive: usernames/cns
|
||||
// are normally alphanumeric, but this prevents metacharacter injection.
|
||||
function escapeLDAPDNValue(val) {
|
||||
return String(val)
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/,/g, '\\,')
|
||||
.replace(/\+/g, '\\+')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/</g, '\\<')
|
||||
.replace(/>/g, '\\>')
|
||||
.replace(/;/g, '\\;')
|
||||
.replace(/=/g, '\\=')
|
||||
.replace(/^\s|\s$/g, match => match === ' ' ? '\\ ' : match);
|
||||
}
|
||||
|
||||
function makeClient() {
|
||||
return new Client({ url: conf.url });
|
||||
}
|
||||
@@ -19,7 +44,7 @@ async function withClient(fn) {
|
||||
}
|
||||
|
||||
async function getGroups(client, member){
|
||||
let memberFilter = member ? `(member=${member})`: ''
|
||||
let memberFilter = member ? `(member=${escapeLDAPSearchValue(member)})`: ''
|
||||
|
||||
let groups = (await client.search(conf.groupBase, {
|
||||
scope: 'sub',
|
||||
@@ -35,7 +60,8 @@ async function getGroups(client, member){
|
||||
}
|
||||
|
||||
async function addGroup(client, data){
|
||||
await client.add(`cn=${data.name},${conf.groupBase}`, {
|
||||
const safeName = escapeLDAPDNValue(data.name);
|
||||
await client.add(`cn=${safeName},${conf.groupBase}`, {
|
||||
cn: data.name,
|
||||
member: data.owner,
|
||||
description: data.description,
|
||||
@@ -139,9 +165,10 @@ Group.get = async function(data){
|
||||
}
|
||||
|
||||
return withClient(async (client) => {
|
||||
const safeName = escapeLDAPSearchValue(data.name);
|
||||
let group = (await client.search(conf.groupBase, {
|
||||
scope: 'sub',
|
||||
filter: `(&(objectClass=groupOfNames)(cn=${data.name}))`,
|
||||
filter: `(&(objectClass=groupOfNames)(cn=${safeName}))`,
|
||||
attributes: ['cn', 'description', 'member', 'owner', 'createTimestamp', 'modifyTimestamp'],
|
||||
})).searchEntries[0];
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
const Table = require('.');
|
||||
const bcrypt = require('bcrypt');
|
||||
const UUID = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)};
|
||||
const crypto = require('crypto');
|
||||
const UUID = () => crypto.randomUUID();
|
||||
const conf = require('@simpleworkjs/conf');
|
||||
|
||||
const defaultLifetime = (conf.oauth && conf.oauth.token_lifetime) || {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const Table = require('.');
|
||||
const UUID = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)};
|
||||
const crypto = require('crypto');
|
||||
const UUID = () => crypto.randomUUID();
|
||||
|
||||
// Shared base keyMap matching Token's schema so these behave as tokens
|
||||
const tokenKeyMap = {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const Table = require('.');
|
||||
const UUID = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)};
|
||||
const crypto = require('crypto');
|
||||
const UUID = () => crypto.randomUUID();
|
||||
|
||||
|
||||
class Token extends Table{
|
||||
@@ -110,7 +111,7 @@ class OtpToken extends Token {
|
||||
for (const t of existing) {
|
||||
if (t.is_valid) await t.update({is_valid: false});
|
||||
}
|
||||
const code = String(Math.floor(100000 + Math.random() * 900000));
|
||||
const code = String(crypto.randomInt(100000, 1000000));
|
||||
return this.create({uid, code, method, created_by: uid});
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,20 @@ function escapeLDAPSearchValue(val) {
|
||||
.replace(/\0/g, '\\00');
|
||||
}
|
||||
|
||||
// Escape a value used in an LDAP DN (RFC 4514).
|
||||
function escapeLDAPDNValue(val) {
|
||||
return String(val)
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/,/g, '\\,')
|
||||
.replace(/\+/g, '\\+')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/</g, '\\<')
|
||||
.replace(/>/g, '\\>')
|
||||
.replace(/;/g, '\\;')
|
||||
.replace(/=/g, '\\=')
|
||||
.replace(/^\s|\s$/g, match => match === ' ' ? '\\ ' : match);
|
||||
}
|
||||
|
||||
// Compute the next available uid/gidNumber: the highest existing value below
|
||||
// conf.uidGidReservedFloor, plus one -- or conf.uidGidMin if there are no
|
||||
// such entries yet. Entries at/above the reserved floor (e.g. a bootstrap
|
||||
@@ -72,7 +86,8 @@ async function addPosixGroup(client, data){
|
||||
|
||||
data.gidNumber = nextPosixId(groups, 'gidNumber');
|
||||
|
||||
await client.add(`cn=${data.cn},${conf.groupBase}`, {
|
||||
const safeCn = escapeLDAPDNValue(data.cn);
|
||||
await client.add(`cn=${safeCn},${conf.groupBase}`, {
|
||||
cn: data.cn,
|
||||
gidNumber: data.gidNumber,
|
||||
objectclass: [ 'posixGroup', 'top' ]
|
||||
@@ -94,6 +109,7 @@ async function addPosixAccount(client, data){
|
||||
|
||||
data.uidNumber = nextPosixId(people, 'uidNumber');
|
||||
|
||||
const safeCn = escapeLDAPDNValue(data.cn);
|
||||
const entry = {
|
||||
cn: data.cn,
|
||||
sn: data.sn,
|
||||
@@ -143,7 +159,7 @@ async function addPosixAccount(client, data){
|
||||
entry.manager = [].concat(data.manager);
|
||||
}
|
||||
|
||||
await client.add(`cn=${data.cn},${conf.userBase}`, entry);
|
||||
await client.add(`cn=${safeCn},${conf.userBase}`, entry);
|
||||
|
||||
return data
|
||||
|
||||
@@ -171,7 +187,6 @@ async function addLdapUser(client, data){
|
||||
delete data.userPassword;
|
||||
}
|
||||
|
||||
console.log('addLdapUser', data)
|
||||
group = await addPosixGroup(client, data);
|
||||
data = await addPosixAccount(client, group);
|
||||
|
||||
@@ -799,7 +814,7 @@ User.addSSHkey = async function(data) {
|
||||
// memberUid (RFC 2307, posixGroup) is a bare username, not a DN, unlike
|
||||
// groupOfNames' `member` used by app_sso_* groups in group_ldap.js.
|
||||
function personalGroupDN(uid){
|
||||
return `cn=${uid},${conf.groupBase}`;
|
||||
return `cn=${escapeLDAPDNValue(uid)},${conf.groupBase}`;
|
||||
}
|
||||
|
||||
User.getPersonalGroupMembers = async function(uid) {
|
||||
@@ -873,7 +888,7 @@ User.login = async function(data){
|
||||
return user;
|
||||
|
||||
}catch(error){
|
||||
console.error("USER LOGIN error:", error);
|
||||
console.error("USER LOGIN error:", error.name, error.message);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
Generated
+40
-11
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "t42-sso-manager",
|
||||
"version": "1.1.12",
|
||||
"version": "1.1.16",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "t42-sso-manager",
|
||||
"version": "1.1.12",
|
||||
"version": "1.1.16",
|
||||
"license": "MIT",
|
||||
"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",
|
||||
@@ -19,7 +19,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",
|
||||
@@ -30,7 +30,8 @@
|
||||
"mustache": "^4.2.0",
|
||||
"nodemailer": "^9.0.0",
|
||||
"p2psub": "^0.2.0",
|
||||
"socket.io": "^4.8.3"
|
||||
"socket.io": "^4.8.3",
|
||||
"xss": "^1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^30.4.2",
|
||||
@@ -1127,9 +1128,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"
|
||||
@@ -2331,6 +2332,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/component-emitter": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
|
||||
@@ -2488,6 +2495,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/cssfilter": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
|
||||
"integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
@@ -4357,9 +4370,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"
|
||||
@@ -6488,6 +6501,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xss": {
|
||||
"version": "1.0.15",
|
||||
"resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz",
|
||||
"integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^2.20.3",
|
||||
"cssfilter": "0.0.10"
|
||||
},
|
||||
"bin": {
|
||||
"xss": "bin/xss"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
||||
+5
-5
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "t42-sso-manager",
|
||||
"version": "1.1.12",
|
||||
"private": true,
|
||||
"version": "1.1.16",
|
||||
"author": [
|
||||
{
|
||||
"name": "William Mantly",
|
||||
@@ -23,7 +22,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 +30,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",
|
||||
@@ -42,7 +41,8 @@
|
||||
"mustache": "^4.2.0",
|
||||
"nodemailer": "^9.0.0",
|
||||
"p2psub": "^0.2.0",
|
||||
"socket.io": "^4.8.3"
|
||||
"socket.io": "^4.8.3",
|
||||
"xss": "^1.0.15"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
+15
-2
@@ -4,6 +4,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const router = require('express').Router();
|
||||
const {marked} = require('marked');
|
||||
const xss = require('xss');
|
||||
const conf = require('@simpleworkjs/conf');
|
||||
const buildInfo = require('../utils/build_info');
|
||||
const rateLimit = require('../middleware/rate_limit');
|
||||
@@ -60,10 +61,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
|
||||
@@ -119,7 +132,7 @@ router.get('/:slug', function(req, res, next) {
|
||||
docs: docList,
|
||||
currentSlug: req.params.slug,
|
||||
docTitle: doc.title,
|
||||
docHtml: fixDocLinks(fixImagePaths(marked(content))),
|
||||
docHtml: xss(fixDocLinks(fixImagePaths(marked(content)))),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@@ -5,6 +5,7 @@ var express = require('express');
|
||||
var router = express.Router();
|
||||
const moment = require('moment');
|
||||
const {marked} = require('marked');
|
||||
const xss = require('xss');
|
||||
const {InviteToken, PasswordResetToken} = require('./../models/token');
|
||||
const {Tos} = require('../models/tos');
|
||||
const conf = require('@simpleworkjs/conf');
|
||||
@@ -46,7 +47,7 @@ router.get('/health', function(req, res) {
|
||||
router.get('/tos', async function(req, res, next) {
|
||||
try {
|
||||
const tos = await Tos.getCurrent();
|
||||
res.render('tos', {...values, tosHtml: marked(tos.content), tosUpdatedOnFmt: moment(tos.updated_on, 'x').format('MMMM YYYY')});
|
||||
res.render('tos', {...values, tosHtml: xss(marked(tos.content)), tosUpdatedOnFmt: moment(tos.updated_on, 'x').format('MMMM YYYY')});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
@@ -68,7 +69,7 @@ router.get('/invites', function(req, res) {
|
||||
router.get('/onboarding', async function(req, res, next) {
|
||||
try {
|
||||
const tos = await Tos.getCurrent();
|
||||
res.render('onboarding', {...values, tosHtml: marked(tos.content)});
|
||||
res.render('onboarding', {...values, tosHtml: xss(marked(tos.content))});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
|
||||
Binary file not shown.
+2
-2
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user