Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a8030fd7d | |||
| cf80c966eb | |||
| 07819a6254 | |||
| 1b3e842006 | |||
| efe3e514b0 | |||
| 37f2ece172 | |||
| b77704089b | |||
| 114d8c86ca | |||
| 3e87ad86ab | |||
| c5a2c0a71d | |||
| fe23d231be | |||
| 3a612dbed7 | |||
| f154bb8db0 | |||
| 9fc5abda2a | |||
| a6ee985de4 |
@@ -136,6 +136,10 @@ jobs:
|
|||||||
# directory layout (dc=example,dc=com) -- only the admin password
|
# directory layout (dc=example,dc=com) -- only the admin password
|
||||||
# (normally supplied via a gitignored secrets.js) needs setting.
|
# (normally supplied via a gitignored secrets.js) needs setting.
|
||||||
app_ldap__bindPassword: your-ldap-password
|
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
|
run: npm test
|
||||||
|
|
||||||
test-summary:
|
test-summary:
|
||||||
|
|||||||
+38
-4
@@ -6,12 +6,44 @@ correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [1.1.13] - 2026-07-17
|
||||||
|
|
||||||
### Fixed
|
### 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.
|
- 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.
|
||||||
|
|
||||||
Bumps to v1.1.13.
|
## [1.1.12] - 2026-07-17
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Three new plain-language docs aimed at less technical readers, replacing the schema-level LDAP/OAuth/API docs as the target of most card help links: **Accounts, Groups & Managers**, **Connecting Apps (SSO)**, and **API Tokens**. Each links onward to the deeper technical reference for readers who want it; the technical docs link back the other way too. The personal-access-token card (previously missed) now links to its own doc.
|
- Three new plain-language docs aimed at less technical readers, replacing the schema-level LDAP/OAuth/API docs as the target of most card help links: **Accounts, Groups & Managers**, **Connecting Apps (SSO)**, and **API Tokens**. Each links onward to the deeper technical reference for readers who want it; the technical docs link back the other way too. The personal-access-token card (previously missed) now links to its own doc.
|
||||||
@@ -19,12 +51,12 @@ Bumps to v1.1.13.
|
|||||||
### Fixed
|
### 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.
|
- 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
|
### 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.
|
- 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
|
### 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).
|
- 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).
|
||||||
@@ -101,7 +133,9 @@ 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.13...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.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
|
||||||
[1.1.11]: https://github.com/theta42/sso-manager-node/compare/v1.1.10...v1.1.11
|
[1.1.11]: https://github.com/theta42/sso-manager-node/compare/v1.1.10...v1.1.11
|
||||||
|
|||||||
+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
|
||||||
|
|
||||||
|
|||||||
+13
-12
@@ -6,12 +6,12 @@
|
|||||||
# production, run a dedicated LDAP server and point the app at it via app_*
|
# 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.
|
# 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
|
# The app reads its configuration from conf/base.js + a secrets file, deep-merged
|
||||||
# by @simpleworkjs/conf, with `app_*` environment variables as the
|
# by @simpleworkjs/conf (requires >= 1.2.0, pinned in nodejs/package-lock.json),
|
||||||
# highest-precedence override layer. This entrypoint exports those `app_*`
|
# with `app_*` environment variables as the highest-precedence override layer.
|
||||||
# vars so the app connects to the bundled slapd without any mounted secrets
|
# This entrypoint exports those `app_*` vars so the app connects to the bundled
|
||||||
# file. Any `app_*` var already set in the environment wins (the values below
|
# slapd without any mounted secrets file. Any `app_*` var already set in the
|
||||||
# are defaults/fallbacks only).
|
# environment wins (the values below are defaults/fallbacks only).
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@@ -33,14 +33,15 @@ error() { echo "[ERROR] $*" >&2; }
|
|||||||
# ── Optional: load operational config from a mounted secrets.js ──────────────
|
# ── Optional: load operational config from a mounted secrets.js ──────────────
|
||||||
# The unified theta-env stack mounts ./config/sso-secrets.js at /config and
|
# 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
|
# treats it as the authoritative source for the SSO's config (LDAP base, admin
|
||||||
# password, org name, JWT secret, ...). When present, symlink it into
|
# password, org name, JWT secret, ...). When present, point CONF_SECRETS at it
|
||||||
# /app/conf/secrets.js so @simpleworkjs/conf reads it, and override the
|
# so @simpleworkjs/conf reads it directly (no write access to /app/conf
|
||||||
# env-derived operational vars below with the file's values. When absent
|
# needed), and override the env-derived operational vars below with the
|
||||||
# (standalone / env-var deployments) the env vars set above stay in effect and
|
# file's values. When absent (standalone / env-var deployments) the env vars
|
||||||
# the app_* exports further down are emitted as before.
|
# set above stay in effect and the app_* exports further down are emitted as
|
||||||
|
# before.
|
||||||
SECRETS_JS_MODE=0
|
SECRETS_JS_MODE=0
|
||||||
if [[ -f /config/sso-secrets.js ]]; then
|
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
|
SECRETS_JS_MODE=1
|
||||||
# Pull the entrypoint's operational vars out of secrets.js in one node call.
|
# 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
|
# Node emits `KEY<TAB>base64(value)` lines; we decode each with base64 -d and
|
||||||
|
|||||||
+213
-702
@@ -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)"
|
exit 1
|
||||||
|
|
||||||
# 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
|
|
||||||
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
fi
|
||||||
|
|
||||||
# Derive the DNS domain from the base DN (dc=foo,dc=bar -> foo.bar) for email
|
# FIRST_RUN gates OpenLDAP bootstrap + secrets seeding below -- both only ever
|
||||||
# sender defaults. Override with LDAP_DOMAIN if set.
|
# happen once, the first time this script runs on a host (i.e. before
|
||||||
if [[ -z "${LDAP_DOMAIN:-}" ]]; then
|
# $SECRETS_FILE exists). Every later run only updates the code.
|
||||||
LDAP_DOMAIN=$(echo "$BASE_DN" | sed 's/^dc=//; s/,dc=/./g')
|
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
|
||||||
|
|
||||||
# ── System checks ─────────────────────────────────────────────────────────────
|
NEW_VERSION="$(pkg_version "$REPO_DIR/nodejs/package.json")"
|
||||||
check_root() {
|
|
||||||
if [[ $EUID -ne 0 ]]; then
|
if [ "$FIRST_RUN" -eq 1 ] && [ "$SKIP_LDAP" != "true" ]; then
|
||||||
error "This script must be run as root (sudo)"
|
echo "==> First run: bootstrapping OpenLDAP (base DN: ${LDAP_BASE_DN})"
|
||||||
exit 1
|
LDAP_ADMIN_PASS="${LDAP_ADMIN_PASS:-$(openssl rand -base64 24 | tr -d '=+/')}"
|
||||||
fi
|
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
|
||||||
check_os() {
|
# DN -- "dc=foo,dc=bar" -> "foo.bar". A malformed value here (e.g. the raw
|
||||||
if [[ ! -f /etc/debian_version ]]; then
|
# DN with only the leading "dc=" stripped) makes slapd's postinst hang
|
||||||
error "This script is for Debian/Ubuntu systems only"
|
# indefinitely instead of failing cleanly.
|
||||||
exit 1
|
LDAP_DOMAIN="$(echo "$LDAP_BASE_DN" | sed 's/^dc=//; s/,dc=/./g')"
|
||||||
fi
|
|
||||||
info "Detected $(cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)"
|
if ! command -v slapd >/dev/null 2>&1; then
|
||||||
}
|
debconf-set-selections <<-EOF
|
||||||
|
slapd slapd/internal/adminpw password ${LDAP_ADMIN_PASS}
|
||||||
# ── Package installation ──────────────────────────────────────────────────────
|
slapd slapd/password1 password ${LDAP_ADMIN_PASS}
|
||||||
install_package() {
|
slapd slapd/password2 password ${LDAP_ADMIN_PASS}
|
||||||
local pkg="$1"
|
slapd slapd/domain string ${LDAP_DOMAIN}
|
||||||
if dpkg -l | grep -q "^ii $pkg "; then
|
slapd shared/organization string ${ORG_NAME}
|
||||||
info "Package $pkg is already installed"
|
slapd slapd/purge_database boolean true
|
||||||
return 0
|
slapd slapd/move_old_database boolean true
|
||||||
fi
|
EOF
|
||||||
dry_run "Would install package: $pkg"
|
apt-get install -y slapd ldap-utils
|
||||||
[[ "$DRY_RUN" == "true" ]] && return 0
|
cat > /etc/ldap/ldap.conf <<-EOF
|
||||||
apt-get update -qq
|
BASE ${LDAP_BASE_DN}
|
||||||
apt-get install -y -qq "$pkg"
|
URI ldap://localhost
|
||||||
info "Installed $pkg"
|
EOF
|
||||||
}
|
systemctl enable --now slapd
|
||||||
|
else
|
||||||
install_nodejs() {
|
echo " slapd already installed -- assuming it already serves ${LDAP_BASE_DN}"
|
||||||
if command -v node &>/dev/null && node --version | grep -q "v20"; then
|
fi
|
||||||
info "Node.js 20.x is already installed"
|
|
||||||
return 0
|
echo "==> Directory structure (ou=people, ou=groups)"
|
||||||
fi
|
for ou in people groups; do
|
||||||
dry_run "Would install Node.js 20.x"
|
dn="ou=${ou},${LDAP_BASE_DN}"
|
||||||
[[ "$DRY_RUN" == "true" ]] && return 0
|
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"
|
||||||
info "Installing Node.js 20.x..."
|
else
|
||||||
# Use NodeSource repository for Node.js 20.x
|
ldapadd -x -D "$BIND_DN" -w "$LDAP_ADMIN_PASS" -H ldap://localhost <<-EOF
|
||||||
apt-get update -qq
|
dn: ${dn}
|
||||||
apt-get install -y -qq curl gnupg ca-certificates
|
objectClass: organizationalUnit
|
||||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >/dev/null 2>&1
|
ou: ${ou}
|
||||||
apt-get install -y -qq nodejs
|
EOF
|
||||||
info "Installed Node.js $(node --version)"
|
echo " ${dn} created"
|
||||||
}
|
fi
|
||||||
|
done
|
||||||
# ── OpenLDAP installation and configuration ───────────────────────────────────
|
|
||||||
install_openldap() {
|
echo "==> LDAP modules, overlays, schema, policy, SSO groups"
|
||||||
if command -v slapd &>/dev/null; then
|
"$REPO_DIR/ops/ldap-setup.sh" -p "$LDAP_ADMIN_PASS" -b "$LDAP_BASE_DN" -D "$BIND_DN"
|
||||||
info "OpenLDAP is already installed"
|
|
||||||
return 0
|
echo "==> Seeding ${SECRETS_FILE}"
|
||||||
fi
|
install -d -m 0750 "$(dirname "$SECRETS_FILE")"
|
||||||
dry_run "Would install OpenLDAP"
|
cat > "$SECRETS_FILE" <<-SECRETSEOF
|
||||||
[[ "$DRY_RUN" == "true" ]] && return 0
|
'use strict';
|
||||||
|
|
||||||
info "Installing OpenLDAP..."
|
// 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.
|
||||||
# Pre-seed debconf for non-interactive installation
|
// LDAP admin password + JWT secret below were auto-generated; SMTP is a
|
||||||
debconf-set-selections << EOF
|
// placeholder (email delivery won't work until you fill it in).
|
||||||
slapd slapd/internal/adminpw string $ADMIN_PASS
|
|
||||||
slapd slapd/password1 string $ADMIN_PASS
|
module.exports = {
|
||||||
slapd slapd/password2 string $ADMIN_PASS
|
port: ${PORT},
|
||||||
slapd slapd/domain string ${BASE_DN#dc=}
|
name: '${ORG_NAME}',
|
||||||
slapd slapd/backend string MDB
|
ldap: {
|
||||||
slapd shared/organization string $ORG_NAME
|
url: 'ldap://localhost',
|
||||||
slapd slapd/purge_database boolean true
|
bindDN: '${BIND_DN}',
|
||||||
slapd slapd/move_old_database boolean true
|
bindPassword: '${LDAP_ADMIN_PASS}',
|
||||||
slapd slapd/invalid_config boolean true
|
userBase: 'ou=people,${LDAP_BASE_DN}',
|
||||||
EOF
|
groupBase: 'ou=groups,${LDAP_BASE_DN}',
|
||||||
|
},
|
||||||
apt-get update -qq
|
smtp: {
|
||||||
apt-get install -y -qq slapd ldap-utils
|
host: 'smtp.example.com',
|
||||||
|
port: 587,
|
||||||
# Configure ldap.conf
|
secure: false,
|
||||||
cat > /etc/ldap/ldap.conf << LDAPCONF
|
user: 'noreply@${LDAP_DOMAIN}',
|
||||||
BASE $BASE_DN
|
pass: 'set-me',
|
||||||
URI ldap://localhost
|
from: '${ORG_NAME} <noreply@${LDAP_DOMAIN}>',
|
||||||
LDAPCONF
|
},
|
||||||
|
oauth: {
|
||||||
# Set proper permissions
|
issuer: '',
|
||||||
chmod 644 /etc/ldap/ldap.conf
|
jwtSecret: '${JWT_SECRET}',
|
||||||
|
token_lifetime: {
|
||||||
info "OpenLDAP installed"
|
access_token: 3600,
|
||||||
}
|
refresh_token: 2592000,
|
||||||
|
},
|
||||||
configure_openldap() {
|
},
|
||||||
info "Configuring OpenLDAP..."
|
};
|
||||||
dry_run "Would configure OpenLDAP with base DN: $BASE_DN"
|
SECRETSEOF
|
||||||
[[ "$DRY_RUN" == "true" ]] && return 0
|
chmod 600 "$SECRETS_FILE"
|
||||||
|
echo " seeded ${SECRETS_FILE} (LDAP + JWT are live; SMTP is a placeholder)"
|
||||||
# Wait for slapd to be ready
|
echo " \$EDITOR ${SECRETS_FILE}"
|
||||||
for i in {1..10}; do
|
echo " then re-run this script (or: sudo systemctl restart sso-manager)"
|
||||||
if ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" "(objectClass=*)" dn >/dev/null 2>&1; then
|
elif [ "$FIRST_RUN" -eq 1 ]; then
|
||||||
info "OpenLDAP is ready"
|
echo "==> SKIP_LDAP=true -- not bootstrapping OpenLDAP or seeding ${SECRETS_FILE}"
|
||||||
break
|
echo " Write it yourself (see secrets.js.example) before starting sso-manager."
|
||||||
fi
|
else
|
||||||
sleep 1
|
echo "==> ${SECRETS_FILE} already exists, leaving LDAP + secrets untouched"
|
||||||
done
|
fi
|
||||||
|
|
||||||
# Detect the database DN for our suffix
|
echo "==> Symlink systemd config from the repo"
|
||||||
DB_DN=$(ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" \
|
link "$REPO_DIR/ops/systemd/sso-manager.service" /etc/systemd/system/sso-manager.service
|
||||||
"(&(objectClass=olcDatabaseConfig)(olcSuffix=${BASE_DN}))" dn 2>/dev/null \
|
|
||||||
| grep "^dn:" | head -1 | sed 's/^dn: //')
|
echo "==> Node dependencies"
|
||||||
|
# Deterministic, production-only install from the lockfile. Falls back to a
|
||||||
if [[ -z "$DB_DN" ]]; then
|
# plain install if the lockfile and manifest are out of step.
|
||||||
# Try to find any database and update its suffix
|
( cd "$REPO_DIR/nodejs" && { npm ci --omit=dev || npm install --omit=dev; } )
|
||||||
DB_DN=$(ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" \
|
|
||||||
"(objectClass=olcDatabaseConfig)" dn 2>/dev/null \
|
echo "==> Services"
|
||||||
| grep "^dn:" | head -1 | sed 's/^dn: //')
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now sso-manager.service
|
||||||
if [[ -n "$DB_DN" ]]; then
|
systemctl restart sso-manager.service
|
||||||
info "Updating database suffix to $BASE_DN"
|
|
||||||
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF
|
echo "==> Done."
|
||||||
dn: $DB_DN
|
if [ -z "$CURRENT_VERSION" ]; then
|
||||||
changetype: modify
|
echo " Installed v${NEW_VERSION}."
|
||||||
replace: olcSuffix
|
elif [ "$CURRENT_VERSION" = "$NEW_VERSION" ]; then
|
||||||
olcSuffix: $BASE_DN
|
echo " Already up to date (v${NEW_VERSION})."
|
||||||
EOF
|
else
|
||||||
fi
|
echo " Updated v${CURRENT_VERSION} -> v${NEW_VERSION}."
|
||||||
fi
|
fi
|
||||||
|
echo " Update later with: sudo BRANCH=${BRANCH} $0"
|
||||||
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
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ Auth.login = async function(data){
|
|||||||
|
|
||||||
return {user, token}
|
return {user, token}
|
||||||
}catch(error){
|
}catch(error){
|
||||||
console.error("AUTH LOGIN error:", error);
|
console.error("AUTH LOGIN error:", error.name, error.message);
|
||||||
throw this.errors.login();
|
throw this.errors.login();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ Mail.sendTemplate = async function(to, template, context, from){
|
|||||||
to,
|
to,
|
||||||
mustache.render(template.subject, context),
|
mustache.render(template.subject, context),
|
||||||
mustache.render(template.message, 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 { LRUCache } = require('lru-cache');
|
||||||
const conf = require('@simpleworkjs/conf').ldap;
|
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() {
|
function makeClient() {
|
||||||
return new Client({ url: conf.url });
|
return new Client({ url: conf.url });
|
||||||
}
|
}
|
||||||
@@ -19,7 +44,7 @@ async function withClient(fn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getGroups(client, member){
|
async function getGroups(client, member){
|
||||||
let memberFilter = member ? `(member=${member})`: ''
|
let memberFilter = member ? `(member=${escapeLDAPSearchValue(member)})`: ''
|
||||||
|
|
||||||
let groups = (await client.search(conf.groupBase, {
|
let groups = (await client.search(conf.groupBase, {
|
||||||
scope: 'sub',
|
scope: 'sub',
|
||||||
@@ -35,7 +60,8 @@ async function getGroups(client, member){
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function addGroup(client, data){
|
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,
|
cn: data.name,
|
||||||
member: data.owner,
|
member: data.owner,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
@@ -139,9 +165,10 @@ Group.get = async function(data){
|
|||||||
}
|
}
|
||||||
|
|
||||||
return withClient(async (client) => {
|
return withClient(async (client) => {
|
||||||
|
const safeName = escapeLDAPSearchValue(data.name);
|
||||||
let group = (await client.search(conf.groupBase, {
|
let group = (await client.search(conf.groupBase, {
|
||||||
scope: 'sub',
|
scope: 'sub',
|
||||||
filter: `(&(objectClass=groupOfNames)(cn=${data.name}))`,
|
filter: `(&(objectClass=groupOfNames)(cn=${safeName}))`,
|
||||||
attributes: ['cn', 'description', 'member', 'owner', 'createTimestamp', 'modifyTimestamp'],
|
attributes: ['cn', 'description', 'member', 'owner', 'createTimestamp', 'modifyTimestamp'],
|
||||||
})).searchEntries[0];
|
})).searchEntries[0];
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
const Table = require('.');
|
const Table = require('.');
|
||||||
const bcrypt = require('bcrypt');
|
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 conf = require('@simpleworkjs/conf');
|
||||||
|
|
||||||
const defaultLifetime = (conf.oauth && conf.oauth.token_lifetime) || {
|
const defaultLifetime = (conf.oauth && conf.oauth.token_lifetime) || {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const Table = require('.');
|
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
|
// Shared base keyMap matching Token's schema so these behave as tokens
|
||||||
const tokenKeyMap = {
|
const tokenKeyMap = {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const Table = require('.');
|
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{
|
class Token extends Table{
|
||||||
@@ -110,7 +111,7 @@ class OtpToken extends Token {
|
|||||||
for (const t of existing) {
|
for (const t of existing) {
|
||||||
if (t.is_valid) await t.update({is_valid: false});
|
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});
|
return this.create({uid, code, method, created_by: uid});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,20 @@ function escapeLDAPSearchValue(val) {
|
|||||||
.replace(/\0/g, '\\00');
|
.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
|
// Compute the next available uid/gidNumber: the highest existing value below
|
||||||
// conf.uidGidReservedFloor, plus one -- or conf.uidGidMin if there are no
|
// conf.uidGidReservedFloor, plus one -- or conf.uidGidMin if there are no
|
||||||
// such entries yet. Entries at/above the reserved floor (e.g. a bootstrap
|
// 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');
|
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,
|
cn: data.cn,
|
||||||
gidNumber: data.gidNumber,
|
gidNumber: data.gidNumber,
|
||||||
objectclass: [ 'posixGroup', 'top' ]
|
objectclass: [ 'posixGroup', 'top' ]
|
||||||
@@ -94,6 +109,7 @@ async function addPosixAccount(client, data){
|
|||||||
|
|
||||||
data.uidNumber = nextPosixId(people, 'uidNumber');
|
data.uidNumber = nextPosixId(people, 'uidNumber');
|
||||||
|
|
||||||
|
const safeCn = escapeLDAPDNValue(data.cn);
|
||||||
const entry = {
|
const entry = {
|
||||||
cn: data.cn,
|
cn: data.cn,
|
||||||
sn: data.sn,
|
sn: data.sn,
|
||||||
@@ -143,7 +159,7 @@ async function addPosixAccount(client, data){
|
|||||||
entry.manager = [].concat(data.manager);
|
entry.manager = [].concat(data.manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.add(`cn=${data.cn},${conf.userBase}`, entry);
|
await client.add(`cn=${safeCn},${conf.userBase}`, entry);
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -171,7 +187,6 @@ async function addLdapUser(client, data){
|
|||||||
delete data.userPassword;
|
delete data.userPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('addLdapUser', data)
|
|
||||||
group = await addPosixGroup(client, data);
|
group = await addPosixGroup(client, data);
|
||||||
data = await addPosixAccount(client, group);
|
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
|
// memberUid (RFC 2307, posixGroup) is a bare username, not a DN, unlike
|
||||||
// groupOfNames' `member` used by app_sso_* groups in group_ldap.js.
|
// groupOfNames' `member` used by app_sso_* groups in group_ldap.js.
|
||||||
function personalGroupDN(uid){
|
function personalGroupDN(uid){
|
||||||
return `cn=${uid},${conf.groupBase}`;
|
return `cn=${escapeLDAPDNValue(uid)},${conf.groupBase}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
User.getPersonalGroupMembers = async function(uid) {
|
User.getPersonalGroupMembers = async function(uid) {
|
||||||
@@ -873,7 +888,7 @@ User.login = async function(data){
|
|||||||
return user;
|
return user;
|
||||||
|
|
||||||
}catch(error){
|
}catch(error){
|
||||||
console.error("USER LOGIN error:", error);
|
console.error("USER LOGIN error:", error.name, error.message);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+40
-11
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "t42-sso-manager",
|
"name": "t42-sso-manager",
|
||||||
"version": "1.1.13",
|
"version": "1.1.16",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "t42-sso-manager",
|
"name": "t42-sso-manager",
|
||||||
"version": "1.1.13",
|
"version": "1.1.16",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^7.3.0",
|
"@fortawesome/fontawesome-free": "^7.3.0",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"@simpleworkjs/conf": "^1.1.0",
|
"@simpleworkjs/conf": "^1.2.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"express-rate-limit": "^8.5.2",
|
"express-rate-limit": "^8.5.2",
|
||||||
"extend": "^3.0.2",
|
"extend": "^3.0.2",
|
||||||
"jq-repeat": "^2.1.0",
|
"jq-repeat": "^2.2.0",
|
||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"ldapts": "^8.1.2",
|
"ldapts": "^8.1.2",
|
||||||
@@ -30,7 +30,8 @@
|
|||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
"nodemailer": "^9.0.0",
|
"nodemailer": "^9.0.0",
|
||||||
"p2psub": "^0.2.0",
|
"p2psub": "^0.2.0",
|
||||||
"socket.io": "^4.8.3"
|
"socket.io": "^4.8.3",
|
||||||
|
"xss": "^1.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jest": "^30.4.2",
|
"jest": "^30.4.2",
|
||||||
@@ -1127,9 +1128,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@simpleworkjs/conf": {
|
"node_modules/@simpleworkjs/conf": {
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@simpleworkjs/conf/-/conf-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@simpleworkjs/conf/-/conf-1.2.0.tgz",
|
||||||
"integrity": "sha512-MKRQQ4JAH2tbEm87NdkmfikTT58Tyk/SFbvCC7zKja0bK6j8zYyBXTQUJ0rnvFOVEalDWd/au4AEiptOCEqgvA==",
|
"integrity": "sha512-X4u1oRb0A0x7wzmyiIH5hPYYIFJYUXhYVe9CPX6G6INouRIeZuHlx0pthHlihiAAIc3+KqZBx18qirFN8RoJwA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"extend": "^3.0.2"
|
"extend": "^3.0.2"
|
||||||
@@ -2331,6 +2332,12 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/component-emitter": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
|
||||||
@@ -2488,6 +2495,12 @@
|
|||||||
"node": ">= 8"
|
"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": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -4357,9 +4370,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jq-repeat": {
|
"node_modules/jq-repeat": {
|
||||||
"version": "2.1.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/jq-repeat/-/jq-repeat-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/jq-repeat/-/jq-repeat-2.2.0.tgz",
|
||||||
"integrity": "sha512-e1OmSWeBEHEtyOhNVysx0bnT5wd6HlZ37JZgPcGPmACJ0K9bXDPq0xOwrM1slQMSTw7FOSNDX+MD6VwvPeeZyQ==",
|
"integrity": "sha512-OdKAQJ8SOTZzoNL/76o5+WJehXnMCoP8aXbDtZCmDh3vuGGdXfN14FkPTqLpZC5xmlv+QVfTXu/UaIRsDjVuhA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"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": {
|
"node_modules/y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
|||||||
+5
-5
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "t42-sso-manager",
|
"name": "t42-sso-manager",
|
||||||
"version": "1.1.13",
|
"version": "1.1.16",
|
||||||
"private": true,
|
|
||||||
"author": [
|
"author": [
|
||||||
{
|
{
|
||||||
"name": "William Mantly",
|
"name": "William Mantly",
|
||||||
@@ -23,7 +22,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^7.3.0",
|
"@fortawesome/fontawesome-free": "^7.3.0",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"@simpleworkjs/conf": "^1.1.0",
|
"@simpleworkjs/conf": "^1.2.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
@@ -31,7 +30,7 @@
|
|||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"express-rate-limit": "^8.5.2",
|
"express-rate-limit": "^8.5.2",
|
||||||
"extend": "^3.0.2",
|
"extend": "^3.0.2",
|
||||||
"jq-repeat": "^2.1.0",
|
"jq-repeat": "^2.2.0",
|
||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"ldapts": "^8.1.2",
|
"ldapts": "^8.1.2",
|
||||||
@@ -42,7 +41,8 @@
|
|||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
"nodemailer": "^9.0.0",
|
"nodemailer": "^9.0.0",
|
||||||
"p2psub": "^0.2.0",
|
"p2psub": "^0.2.0",
|
||||||
"socket.io": "^4.8.3"
|
"socket.io": "^4.8.3",
|
||||||
|
"xss": "^1.0.15"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const router = require('express').Router();
|
const router = require('express').Router();
|
||||||
const {marked} = require('marked');
|
const {marked} = require('marked');
|
||||||
|
const xss = require('xss');
|
||||||
const conf = require('@simpleworkjs/conf');
|
const conf = require('@simpleworkjs/conf');
|
||||||
const buildInfo = require('../utils/build_info');
|
const buildInfo = require('../utils/build_info');
|
||||||
const rateLimit = require('../middleware/rate_limit');
|
const rateLimit = require('../middleware/rate_limit');
|
||||||
@@ -131,7 +132,7 @@ router.get('/:slug', function(req, res, next) {
|
|||||||
docs: docList,
|
docs: docList,
|
||||||
currentSlug: req.params.slug,
|
currentSlug: req.params.slug,
|
||||||
docTitle: doc.title,
|
docTitle: doc.title,
|
||||||
docHtml: fixDocLinks(fixImagePaths(marked(content))),
|
docHtml: xss(fixDocLinks(fixImagePaths(marked(content)))),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ var express = require('express');
|
|||||||
var router = express.Router();
|
var router = express.Router();
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const {marked} = require('marked');
|
const {marked} = require('marked');
|
||||||
|
const xss = require('xss');
|
||||||
const {InviteToken, PasswordResetToken} = require('./../models/token');
|
const {InviteToken, PasswordResetToken} = require('./../models/token');
|
||||||
const {Tos} = require('../models/tos');
|
const {Tos} = require('../models/tos');
|
||||||
const conf = require('@simpleworkjs/conf');
|
const conf = require('@simpleworkjs/conf');
|
||||||
@@ -46,7 +47,7 @@ router.get('/health', function(req, res) {
|
|||||||
router.get('/tos', async function(req, res, next) {
|
router.get('/tos', async function(req, res, next) {
|
||||||
try {
|
try {
|
||||||
const tos = await Tos.getCurrent();
|
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) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -68,7 +69,7 @@ router.get('/invites', function(req, res) {
|
|||||||
router.get('/onboarding', async function(req, res, next) {
|
router.get('/onboarding', async function(req, res, next) {
|
||||||
try {
|
try {
|
||||||
const tos = await Tos.getCurrent();
|
const tos = await Tos.getCurrent();
|
||||||
res.render('onboarding', {...values, tosHtml: marked(tos.content)});
|
res.render('onboarding', {...values, tosHtml: xss(marked(tos.content))});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
+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