Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37f2ece172 | |||
| b77704089b | |||
| 114d8c86ca | |||
| 3e87ad86ab |
+12
-1
@@ -6,6 +6,16 @@ correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [1.1.14] - 2026-07-17
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@@ -106,7 +116,8 @@ 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.
|
- Unix/POSIX and LDAP bind-only service account support, distinct from real-person accounts.
|
||||||
- Merged OAuth Apps + LDAP Info into a single Integrations page.
|
- Merged OAuth Apps + LDAP Info into a single Integrations page.
|
||||||
|
|
||||||
[Unreleased]: https://github.com/theta42/sso-manager-node/compare/v1.1.14...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.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.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.12]: https://github.com/theta42/sso-manager-node/compare/v1.1.11...v1.1.12
|
||||||
|
|||||||
+42
-32
@@ -332,10 +332,17 @@ OAuth clients live in SSO Redis and are preserved by the volume.
|
|||||||
|
|
||||||
## Method 2: Bare metal (Debian/Ubuntu)
|
## Method 2: Bare metal (Debian/Ubuntu)
|
||||||
|
|
||||||
`install.sh` is an idempotent installer: it installs Node.js 20.x, installs and
|
`install.sh` is an idempotent installer: it installs Node.js 22.x and Redis,
|
||||||
configures OpenLDAP (modules + overlays + custom schema + directory tree +
|
force-syncs the repo to `/opt/theta42/sso-manager`, and symlinks the systemd
|
||||||
required groups), deploys the app to `/opt/sso-manager`, and creates a systemd
|
config from the repo. Re-run it to update — it prints the version you're
|
||||||
unit. Configuration is written to `/opt/sso-manager/conf/secrets.js` (file-based).
|
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
|
### Prerequisites
|
||||||
|
|
||||||
@@ -346,47 +353,50 @@ unit. Configuration is written to `/opt/sso-manager/conf/secrets.js` (file-based
|
|||||||
### Install
|
### Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo ./install.sh \
|
wget -O - https://raw.githubusercontent.com/theta42/sso-manager-node/master/install.sh | sudo bash
|
||||||
-p 'your-ldap-password' \
|
|
||||||
-b 'dc=yourdomain,dc=com' \
|
|
||||||
-n 'Your Org' \
|
|
||||||
-o 3001
|
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Env var | Description |
|
or, if you already have the repo checked out:
|
||||||
|------|---------|-------------|
|
|
||||||
| `-p, --admin-pass` | `LDAP_ADMIN_PASS` | LDAP admin password (required) |
|
```bash
|
||||||
| `-b, --base-dn` | `LDAP_BASE_DN` | Base DN (default `dc=example,dc=com`) |
|
sudo ./install.sh
|
||||||
| `-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) |
|
| Env var | Description |
|
||||||
| `-s, --smtp-config` | `SMTP_*` | SMTP as `host:port:user:pass` |
|
|---------|-------------|
|
||||||
| `--skip-ldap` | `SKIP_LDAP` | Skip LDAP setup (use existing) |
|
| `LDAP_BASE_DN` | Base DN (default `dc=example,dc=com`) — first run only |
|
||||||
| `--skip-app` | `SKIP_APP` | LDAP setup only |
|
| `LDAP_ADMIN_PASS` | LDAP admin password (default auto-generated) — first run only |
|
||||||
| `--dry-run` | `DRY_RUN` | Show actions without making changes |
|
| `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
|
### Post-install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo systemctl enable --now sso-manager
|
sudo systemctl status sso-manager
|
||||||
journalctl -fu sso-manager
|
journalctl -fu sso-manager
|
||||||
curl http://localhost:3001/health # -> {"status":"ok"}
|
curl http://localhost:3001/health # -> {"status":"ok"}
|
||||||
```
|
```
|
||||||
|
|
||||||
### What `install.sh` does
|
### What `install.sh` does
|
||||||
|
|
||||||
1. Installs Node.js 20.x (NodeSource).
|
1. Installs Node.js 22.x (NodeSource) and Redis.
|
||||||
2. Installs OpenLDAP (`slapd`) with: `pw-sha2`, `ppolicy`, `memberof`, `refint`
|
2. Clones/updates the repo at `/opt/theta42/sso-manager`.
|
||||||
modules + overlays; the custom `theta42Person` schema (`dateOfBirth`); indexes;
|
3. **First run only:** installs OpenLDAP (`slapd`) with `pw-sha2`, `ppolicy`,
|
||||||
`ou=people`/`ou=groups`/`ou=policies`; a default `pwdPolicy`; and the SSO groups.
|
`memberof`, `refint` modules + overlays; the custom `theta42Person` schema
|
||||||
3. Installs the app to `/opt/sso-manager` and runs `npm ci --omit=dev`.
|
(`dateOfBirth`); indexes; `ou=people`/`ou=groups`/`ou=policies`; a default
|
||||||
4. Generates `conf/secrets.js` (LDAP/SMTP/JWT) and `conf/base.js` (generic defaults).
|
`pwdPolicy`; and the SSO groups — then seeds `/etc/sso-manager/secrets.js`.
|
||||||
5. Installs `sso-manager.service` (systemd), enabled on boot.
|
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
|
> For an existing LDAP server, run with `SKIP_LDAP=true` and write
|
||||||
> app at it. For LDAP-only setup on a host that already runs the app elsewhere, use
|
> `/etc/sso-manager/secrets.js` yourself (see `secrets.js.example`) before
|
||||||
> `--skip-app`. To (re)configure overlays on an already-installed slapd, prefer
|
> starting the service. To (re)configure overlays on an already-installed
|
||||||
> `ops/ldap-setup.sh` (idempotent, auto-detects the user database).
|
> 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
|
### 3. Bare metal on Debian/Ubuntu
|
||||||
|
|
||||||
`install.sh` is an idempotent installer: it installs Node.js 20.x and OpenLDAP,
|
An automated installer installs Node.js, Redis, and (on first run) OpenLDAP —
|
||||||
configures the directory (modules, overlays, schema, the SSO groups), deploys
|
configuring the directory (modules, overlays, schema, the SSO groups) and
|
||||||
the app to `/opt/sso-manager`, and creates a systemd unit.
|
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
|
||||||
The only thing it requires is the LDAP admin password; the domain (base DN)
|
systemd service:
|
||||||
defaults to `dc=example,dc=com` if you don't pass one:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo ./install.sh -p 'your-ldap-password' -b 'dc=yourdomain,dc=com'
|
wget -O - https://raw.githubusercontent.com/theta42/sso-manager-node/master/install.sh | sudo bash
|
||||||
sudo systemctl enable --now sso-manager
|
|
||||||
curl http://localhost:3001/health # -> {"status":"ok"}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Run `sudo ./install.sh -h` for all flags (`-n` org name, `-o` port, `-j` JWT
|
That's it — LDAP and the app are both live afterward. Edit
|
||||||
secret, `-s` SMTP, `--skip-ldap` to use an existing LDAP, `--dry-run`). Re-run
|
`/etc/sso-manager/secrets.js` (org name, SMTP, a non-default base DN, ...) and
|
||||||
it to update. Full details in [DEPLOYMENT.md](DEPLOYMENT.md) under *Method 2:
|
restart the service to customize. It's idempotent and safe to re-run —
|
||||||
Bare metal*.
|
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
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
+172
-661
@@ -1,719 +1,230 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# install.sh - Idempotent standalone installer for Theta42 SSO Manager
|
|
||||||
# For Debian/Ubuntu systems
|
|
||||||
#
|
#
|
||||||
# This script:
|
# Install / update Theta42 SSO Manager on a fresh or existing host.
|
||||||
# 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
|
|
||||||
#
|
#
|
||||||
# Usage:
|
# This script is idempotent: run it to install, and re-run it to update. It
|
||||||
# sudo ./install.sh [OPTIONS]
|
# 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:
|
# Secrets live at $SECRETS_FILE (/etc/sso-manager/secrets.js by default),
|
||||||
# -p, --admin-pass PASSWORD LDAP admin password (required, or set via LDAP_ADMIN_PASS env)
|
# outside the repo checkout so they survive the hard reset below. FIRST RUN
|
||||||
# -b, --base-dn DN Base DN (default: dc=example,dc=com)
|
# ONLY (no $SECRETS_FILE yet): installs and configures OpenLDAP (modules,
|
||||||
# -n, --org-name NAME Organization name shown in UI/email (default: SSO Manager)
|
# overlays, custom schema, directory tree, required SSO groups -- see
|
||||||
# -o, --port PORT HTTP port for SSO Manager (default: 3001)
|
# ops/ldap-setup.sh), generates an LDAP admin password + JWT secret unless
|
||||||
# -j, --jwt-secret SECRET JWT secret for OAuth (default: auto-generated)
|
# given via env, and seeds $SECRETS_FILE with those values plus SMTP
|
||||||
# -s, --smtp-config CONFIG SMTP config as host:port:user:pass
|
# placeholders. Edit that file (SMTP, org name, ...) and re-run this script to
|
||||||
# --skip-ldap Skip LDAP installation (use existing LDAP)
|
# apply changes -- once it exists it is never touched again, and LDAP is never
|
||||||
# --skip-app Skip application installation (LDAP setup only)
|
# re-bootstrapped.
|
||||||
# --dry-run Show what would be done without making changes
|
|
||||||
# -h, --help Show this help
|
|
||||||
#
|
#
|
||||||
# Environment variables (alternative to flags):
|
# Intended to be driven by CI/CD with no human writes on prod: the checkout is
|
||||||
# LDAP_ADMIN_PASS, LDAP_BASE_DN, PORT, JWT_SECRET, SMTP_*
|
# 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
|
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 ──────────────────────────────────────────────────────────────────
|
REPO_URL="${REPO_URL:-https://github.com/theta42/sso-manager-node.git}"
|
||||||
BASE_DN="${LDAP_BASE_DN:-dc=example,dc=com}"
|
REPO_DIR="${REPO_DIR:-/opt/theta42/sso-manager}"
|
||||||
ADMIN_PASS="${LDAP_ADMIN_PASS:-}"
|
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}"
|
ORG_NAME="${ORG_NAME:-SSO Manager}"
|
||||||
PORT="${PORT:-3001}"
|
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_LDAP="${SKIP_LDAP:-false}"
|
||||||
SKIP_APP="${SKIP_APP:-false}"
|
|
||||||
DRY_RUN="${DRY_RUN:-false}"
|
|
||||||
|
|
||||||
INSTALL_DIR="/opt/sso-manager"
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
SYSTEMD_DIR="/etc/systemd/system"
|
echo "This script must be run as root (try: sudo $0)" >&2
|
||||||
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Generate JWT secret if not provided
|
# Symlink $1 -> $2, replacing whatever is already at $2 (idempotent).
|
||||||
if [[ -z "$JWT_SECRET" ]]; then
|
link(){
|
||||||
JWT_SECRET=$(openssl rand -hex 32)
|
ln -sfn "$1" "$2"
|
||||||
info "Generated JWT secret: ${JWT_SECRET:0:8}..."
|
echo "linked $2 -> $1"
|
||||||
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')
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── System checks ─────────────────────────────────────────────────────────────
|
|
||||||
check_root() {
|
|
||||||
if [[ $EUID -ne 0 ]]; then
|
|
||||||
error "This script must be run as root (sudo)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
check_os() {
|
# Read the "version" field out of a package.json without depending on Node
|
||||||
if [[ ! -f /etc/debian_version ]]; then
|
# being installed yet (this runs before the Node.js install step below).
|
||||||
error "This script is for Debian/Ubuntu systems only"
|
pkg_version(){
|
||||||
exit 1
|
sed -n 's/^[[:space:]]*"version":[[:space:]]*"\([^"]*\)".*/\1/p' "$1" | head -1
|
||||||
fi
|
|
||||||
info "Detected $(cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Package installation ──────────────────────────────────────────────────────
|
# Installed version before this run touches anything, for the upgrade banner
|
||||||
install_package() {
|
# at the end. Empty on a fresh install (no prior checkout).
|
||||||
local pkg="$1"
|
CURRENT_VERSION=""
|
||||||
if dpkg -l | grep -q "^ii $pkg "; then
|
if [ -f "$REPO_DIR/nodejs/package.json" ]; then
|
||||||
info "Package $pkg is already installed"
|
CURRENT_VERSION="$(pkg_version "$REPO_DIR/nodejs/package.json")"
|
||||||
return 0
|
|
||||||
fi
|
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() {
|
# FIRST_RUN gates OpenLDAP bootstrap + secrets seeding below -- both only ever
|
||||||
if command -v node &>/dev/null && node --version | grep -q "v20"; then
|
# happen once, the first time this script runs on a host (i.e. before
|
||||||
info "Node.js 20.x is already installed"
|
# $SECRETS_FILE exists). Every later run only updates the code.
|
||||||
return 0
|
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
|
fi
|
||||||
dry_run "Would install Node.js 20.x"
|
|
||||||
[[ "$DRY_RUN" == "true" ]] && return 0
|
|
||||||
|
|
||||||
info "Installing Node.js 20.x..."
|
NEW_VERSION="$(pkg_version "$REPO_DIR/nodejs/package.json")"
|
||||||
# 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 ───────────────────────────────────
|
if [ "$FIRST_RUN" -eq 1 ] && [ "$SKIP_LDAP" != "true" ]; then
|
||||||
install_openldap() {
|
echo "==> First run: bootstrapping OpenLDAP (base DN: ${LDAP_BASE_DN})"
|
||||||
if command -v slapd &>/dev/null; then
|
LDAP_ADMIN_PASS="${LDAP_ADMIN_PASS:-$(openssl rand -base64 24 | tr -d '=+/')}"
|
||||||
info "OpenLDAP is already installed"
|
JWT_SECRET="${JWT_SECRET:-$(openssl rand -hex 32)}"
|
||||||
return 0
|
BIND_DN="cn=admin,${LDAP_BASE_DN}"
|
||||||
fi
|
# slapd/domain wants a dotted DNS domain (e.g. "example.com"), not the raw
|
||||||
dry_run "Would install OpenLDAP"
|
# DN -- "dc=foo,dc=bar" -> "foo.bar". A malformed value here (e.g. the raw
|
||||||
[[ "$DRY_RUN" == "true" ]] && return 0
|
# 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')"
|
||||||
|
|
||||||
info "Installing OpenLDAP..."
|
if ! command -v slapd >/dev/null 2>&1; then
|
||||||
|
debconf-set-selections <<-EOF
|
||||||
# Pre-seed debconf for non-interactive installation
|
slapd slapd/internal/adminpw password ${LDAP_ADMIN_PASS}
|
||||||
debconf-set-selections << EOF
|
slapd slapd/password1 password ${LDAP_ADMIN_PASS}
|
||||||
slapd slapd/internal/adminpw string $ADMIN_PASS
|
slapd slapd/password2 password ${LDAP_ADMIN_PASS}
|
||||||
slapd slapd/password1 string $ADMIN_PASS
|
slapd slapd/domain string ${LDAP_DOMAIN}
|
||||||
slapd slapd/password2 string $ADMIN_PASS
|
slapd shared/organization string ${ORG_NAME}
|
||||||
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/purge_database boolean true
|
||||||
slapd slapd/move_old_database boolean true
|
slapd slapd/move_old_database boolean true
|
||||||
slapd slapd/invalid_config boolean true
|
|
||||||
EOF
|
EOF
|
||||||
|
apt-get install -y slapd ldap-utils
|
||||||
apt-get update -qq
|
cat > /etc/ldap/ldap.conf <<-EOF
|
||||||
apt-get install -y -qq slapd ldap-utils
|
BASE ${LDAP_BASE_DN}
|
||||||
|
|
||||||
# Configure ldap.conf
|
|
||||||
cat > /etc/ldap/ldap.conf << LDAPCONF
|
|
||||||
BASE $BASE_DN
|
|
||||||
URI ldap://localhost
|
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
|
EOF
|
||||||
|
systemctl enable --now slapd
|
||||||
else
|
else
|
||||||
info "pw-sha2 module already loaded"
|
echo " slapd already installed -- assuming it already serves ${LDAP_BASE_DN}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2. Load ppolicy module
|
echo "==> Directory structure (ou=people, ou=groups)"
|
||||||
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" "(objectClass=olcModuleList)" olcModuleLoad 2>/dev/null | grep -q "ppolicy"; then
|
for ou in people groups; do
|
||||||
info "Loading ppolicy module..."
|
dn="ou=${ou},${LDAP_BASE_DN}"
|
||||||
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF
|
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
|
||||||
dn: cn=module{0},cn=config
|
echo " ${dn} already exists"
|
||||||
changetype: modify
|
|
||||||
add: olcModuleLoad
|
|
||||||
olcModuleLoad: ppolicy
|
|
||||||
EOF
|
|
||||||
else
|
else
|
||||||
info "ppolicy module already loaded"
|
ldapadd -x -D "$BIND_DN" -w "$LDAP_ADMIN_PASS" -H ldap://localhost <<-EOF
|
||||||
fi
|
dn: ${dn}
|
||||||
|
|
||||||
# 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
|
objectClass: organizationalUnit
|
||||||
ou: $ou
|
ou: ${ou}
|
||||||
EOF
|
EOF
|
||||||
else
|
echo " ${dn} created"
|
||||||
info "OU $ou already exists"
|
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# 11. Create default ppolicy
|
echo "==> LDAP modules, overlays, schema, policy, SSO groups"
|
||||||
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
|
"$REPO_DIR/ops/ldap-setup.sh" -p "$LDAP_ADMIN_PASS" -b "$LDAP_BASE_DN" -D "$BIND_DN"
|
||||||
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
|
echo "==> Seeding ${SECRETS_FILE}"
|
||||||
for group in app_sso_admin app_sso_invite app_sso_oauth_admin; do
|
install -d -m 0750 "$(dirname "$SECRETS_FILE")"
|
||||||
dn="cn=$group,ou=groups,$BASE_DN"
|
cat > "$SECRETS_FILE" <<-SECRETSEOF
|
||||||
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';
|
'use strict';
|
||||||
|
|
||||||
module.exports = {
|
// Generated by install.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ). Edit freely --
|
||||||
port: $PORT,
|
// this file is never overwritten by a later run of install.sh.
|
||||||
ldap: {
|
// LDAP admin password + JWT secret below were auto-generated; SMTP is a
|
||||||
url: 'ldap://localhost',
|
// placeholder (email delivery won't work until you fill it in).
|
||||||
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 = {
|
module.exports = {
|
||||||
name: "$ORG_NAME",
|
port: ${PORT},
|
||||||
userModel: 'ldap',
|
name: '${ORG_NAME}',
|
||||||
redis: {
|
|
||||||
prefix: 'sso_manager_'
|
|
||||||
},
|
|
||||||
ldap: {
|
ldap: {
|
||||||
url: 'ldap://localhost',
|
url: 'ldap://localhost',
|
||||||
bindDN: 'cn=admin,$BASE_DN',
|
bindDN: '${BIND_DN}',
|
||||||
bindPassword: '__IN SECRETS FILE__',
|
bindPassword: '${LDAP_ADMIN_PASS}',
|
||||||
userBase: 'ou=people,$BASE_DN',
|
userBase: 'ou=people,${LDAP_BASE_DN}',
|
||||||
groupBase: 'ou=groups,$BASE_DN',
|
groupBase: 'ou=groups,${LDAP_BASE_DN}',
|
||||||
userFilter: '(objectClass=posixAccount)',
|
|
||||||
userNameAttribute: 'uid'
|
|
||||||
},
|
|
||||||
oauth: {
|
|
||||||
issuer: '',
|
|
||||||
jwtSecret: '__in secrets file__',
|
|
||||||
token_lifetime: {
|
|
||||||
access_token: 3600,
|
|
||||||
refresh_token: 2592000
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
smtp: {
|
smtp: {
|
||||||
host: 'localhost',
|
host: 'smtp.example.com',
|
||||||
port: 587,
|
port: 587,
|
||||||
secure: false,
|
secure: false,
|
||||||
from: '$ORG_NAME <noreply@$LDAP_DOMAIN>',
|
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,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
BASEEOF
|
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
|
||||||
|
|
||||||
# Set ownership
|
echo "==> Symlink systemd config from the repo"
|
||||||
chown -R root:root "$INSTALL_DIR"
|
link "$REPO_DIR/ops/systemd/sso-manager.service" /etc/systemd/system/sso-manager.service
|
||||||
chmod -R 755 "$INSTALL_DIR"
|
|
||||||
|
|
||||||
info "Application installed to $INSTALL_DIR"
|
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.
|
||||||
# ── Systemd service configuration ─────────────────────────────────────────────
|
( cd "$REPO_DIR/nodejs" && { npm ci --omit=dev || npm install --omit=dev; } )
|
||||||
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
|
|
||||||
|
|
||||||
|
echo "==> Services"
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable sso-manager.service
|
systemctl enable --now sso-manager.service
|
||||||
|
systemctl restart sso-manager.service
|
||||||
|
|
||||||
info "Systemd service installed"
|
echo "==> Done."
|
||||||
}
|
if [ -z "$CURRENT_VERSION" ]; then
|
||||||
|
echo " Installed v${NEW_VERSION}."
|
||||||
# ── Verification ──────────────────────────────────────────────────────────────
|
elif [ "$CURRENT_VERSION" = "$NEW_VERSION" ]; then
|
||||||
verify_installation() {
|
echo " Already up to date (v${NEW_VERSION})."
|
||||||
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
|
else
|
||||||
warn "✗ OpenLDAP is not running"
|
echo " Updated v${CURRENT_VERSION} -> v${NEW_VERSION}."
|
||||||
((errors++))
|
|
||||||
fi
|
fi
|
||||||
else
|
echo " Update later with: sudo BRANCH=${BRANCH} $0"
|
||||||
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
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "t42-sso-manager",
|
"name": "t42-sso-manager",
|
||||||
"version": "1.1.14",
|
"version": "1.1.15",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "t42-sso-manager",
|
"name": "t42-sso-manager",
|
||||||
"version": "1.1.14",
|
"version": "1.1.15",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^7.3.0",
|
"@fortawesome/fontawesome-free": "^7.3.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "t42-sso-manager",
|
"name": "t42-sso-manager",
|
||||||
"version": "1.1.14",
|
"version": "1.1.15",
|
||||||
"private": true,
|
"private": true,
|
||||||
"author": [
|
"author": [
|
||||||
{
|
{
|
||||||
|
|||||||
+2
-2
@@ -129,7 +129,7 @@ fi
|
|||||||
# ── 3. ppolicy overlay ────────────────────────────────────────────────────────
|
# ── 3. ppolicy overlay ────────────────────────────────────────────────────────
|
||||||
info "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}"
|
skip "ppolicy overlay already configured on ${DB_DN}"
|
||||||
else
|
else
|
||||||
config_add "dn: olcOverlay=ppolicy,${DB_DN}
|
config_add "dn: olcOverlay=ppolicy,${DB_DN}
|
||||||
@@ -280,7 +280,7 @@ info "verifying ppolicy is active on ${DB_DN}"
|
|||||||
|
|
||||||
VERIFY_FAILED=0
|
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"
|
ok "ppolicy overlay is attached to the user database"
|
||||||
else
|
else
|
||||||
warn "ppolicy overlay is NOT attached to ${DB_DN} — active/inactive toggle will fail"
|
warn "ppolicy overlay is NOT attached to ${DB_DN} — active/inactive toggle will fail"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=SSO NodeJS manager Service
|
Description=Theta42 SSO Manager
|
||||||
After=network.target
|
After=network.target slapd.service
|
||||||
|
Wants=slapd.service
|
||||||
StartLimitIntervalSec=0
|
StartLimitIntervalSec=0
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
@@ -8,7 +9,10 @@ Type=simple
|
|||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=1
|
RestartSec=1
|
||||||
User=root
|
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]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
+7
-3
@@ -2,10 +2,14 @@
|
|||||||
|
|
||||||
// Example secrets configuration file (file-based config).
|
// 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
|
// Docker / unified stack: place at ./config/sso-secrets.js and bind-mount
|
||||||
// ./config at /config (see docker-compose.yml); docker-entrypoint.sh symlinks
|
// ./config at /config (see docker-compose.yml); docker-entrypoint.sh points
|
||||||
// it into /app/conf/secrets.js so @simpleworkjs/conf reads it.
|
// 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
|
// 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
|
// vars (if any are set) override this file too — so the Docker stack passes NO
|
||||||
|
|||||||
Reference in New Issue
Block a user