Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3b951b780 | |||
| 8f5ce71bda | |||
| 6de31aa5e0 | |||
| 6c02e6c63e | |||
| e2e8143880 | |||
| aa01a5cc07 | |||
| 1185bb90b8 | |||
| 403e66556c |
@@ -0,0 +1,120 @@
|
|||||||
|
name: CI/CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main", "master" ]
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*'
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main", "master" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-sso-manager:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Use Node.js 20.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20.x
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: sso-manager-node/nodejs/package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: ./sso-manager-node/nodejs
|
||||||
|
run: npm ci || npm install
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
working-directory: ./sso-manager-node/nodejs
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
test-jump-host:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Use Node.js 20.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20.x
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: jump-host/nodejs/package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: ./jump-host/nodejs
|
||||||
|
run: npm ci || npm install
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
working-directory: ./jump-host/nodejs
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
test-proxy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Use Node.js 20.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20.x
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: proxy/nodejs/package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: ./proxy/nodejs
|
||||||
|
run: npm ci || npm install
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
working-directory: ./proxy/nodejs
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
build-telemetry-agent:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
|
||||||
|
- name: Build Agent
|
||||||
|
working-directory: ./telemetry-agent
|
||||||
|
run: go build -v ./...
|
||||||
|
|
||||||
|
docker-push:
|
||||||
|
needs: [test-sso-manager, test-jump-host, test-proxy, build-telemetry-agent]
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and Push SSO Manager
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: ./sso-manager-node
|
||||||
|
file: ./sso-manager-node/Dockerfile.openldap
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
ghcr.io/${{ github.repository_owner }}/sso-manager:latest
|
||||||
|
ghcr.io/${{ github.repository_owner }}/sso-manager:${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Build and Push Proxy
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: ./proxy
|
||||||
|
file: ./proxy/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
ghcr.io/${{ github.repository_owner }}/theta-proxy:latest
|
||||||
|
ghcr.io/${{ github.repository_owner }}/theta-proxy:${{ github.ref_name }}
|
||||||
+258
@@ -1,3 +1,8 @@
|
|||||||
|
# v1.31.0
|
||||||
|
- feat: Integrate full suite CI/CD
|
||||||
|
- feat: Update plugins ecosystem documentation
|
||||||
|
- chore: Bump all submodules to latest tags
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project are documented here. Format loosely
|
All notable changes to this project are documented here. Format loosely
|
||||||
@@ -8,6 +13,259 @@ orchestration code; see each submodule's own `CHANGELOG.md`
|
|||||||
[sso-manager-node](https://github.com/theta42/sso-manager-node/blob/master/CHANGELOG.md))
|
[sso-manager-node](https://github.com/theta42/sso-manager-node/blob/master/CHANGELOG.md))
|
||||||
for what changed inside the apps it composes.
|
for what changed inside the apps it composes.
|
||||||
|
|
||||||
|
## [v1.31.1] - 2026-08-01
|
||||||
|
|
||||||
|
Pairs the sso v1.17.2 post-deploy fixes with the theta-suite half of the
|
||||||
|
`/vault` secrets-list 403 fix (the `sso-admin` OpenBao policy grant that lives
|
||||||
|
in `setup.sh`), and rolls the `sso-manager-node` submodule gitlink to v1.17.2.
|
||||||
|
`proxy` (v1.13.1), `jump-host` (v1.14.1), and `ldap-client` (v1.23.0) are
|
||||||
|
unchanged.
|
||||||
|
|
||||||
|
### Changed (theta-suite)
|
||||||
|
- **`setup.sh` — `sso-admin` policy**: added a `list` grant on the bare KV mount
|
||||||
|
root `secret/metadata` so an admin can list the top-level dirs in the `/vault`
|
||||||
|
UI. `secret/metadata/*` already covered nested paths, but not the mount root
|
||||||
|
itself — so the secrets list 403'd. (The matching per-user/per-app directory
|
||||||
|
grants ship in sso v1.17.2's `vault_broker.js`.)
|
||||||
|
- **`setup.sh` — `ensure_policy`**: now always (re)writes the policy instead of
|
||||||
|
skipping when it exists. `bao policy write` is an idempotent overwrite, so a
|
||||||
|
re-run applies policy edits (like the new grant above) instead of stranding
|
||||||
|
the old HCL with "already exists — keeping."
|
||||||
|
|
||||||
|
### Changed (submodule gitlinks)
|
||||||
|
- **sso-manager-node**: `v1.17.1` → `v1.17.2` — the post-deploy fixes (auto-slug
|
||||||
|
plugins, schedule dropdown, `/profile` rendering, plugin-edit persistence,
|
||||||
|
nmap in the image, the sso-side `/vault` policy grants) plus the SMS (VoIP.ms)
|
||||||
|
and Terms-of-Service configuration on `/conf`. Full changelog below.
|
||||||
|
|
||||||
|
### Deploy
|
||||||
|
Operators upgrading from v1.31.0:
|
||||||
|
1. `git pull` and `git submodule update --init --recursive`.
|
||||||
|
2. Re-run `./setup.sh` — **required**: applies the new `sso-admin`
|
||||||
|
`secret/metadata` list grant and the `ensure_policy` always-write refresh
|
||||||
|
(idempotent). Per-user vault policies self-heal on the next `/vault` visit
|
||||||
|
(sso v1.17.2 re-writes them).
|
||||||
|
3. `docker compose build && docker compose up -d` — the rebuild installs `nmap`
|
||||||
|
in the sso image (fixes the nmap plugin "not found" error).
|
||||||
|
|
||||||
|
### Bundled submodule release notes
|
||||||
|
|
||||||
|
#### sso-manager-node v1.17.2 — post-deploy fixes + SMS/TOS on /conf
|
||||||
|
|
||||||
|
Post-deploy fixes from testing the v1.31.0 stack, plus the SMS (VoIP.ms) and
|
||||||
|
Terms-of-Service configuration the `/conf` page was missing.
|
||||||
|
|
||||||
|
##### Fixed
|
||||||
|
- **Plugin slug is now auto-generated** from the instance name — the New Plugin
|
||||||
|
modal no longer asks for a Slug (it derives a stable, unique handle from the
|
||||||
|
name, appending `-2`, `-3`, … on collision). The generated slug still shows in
|
||||||
|
the table and the Edit (read-only) modal. `POST /api/plugins` `slug` is now
|
||||||
|
optional; an explicit slug is still accepted and validated.
|
||||||
|
- **Plugin schedule is a dropdown**, not a raw cron box: Hourly / Daily /
|
||||||
|
Weekly, plus **Custom** which reveals the raw 5-field cron input. Stored value
|
||||||
|
is still a cron string, so the server is unchanged.
|
||||||
|
- **`/vault` secrets list no longer 403s.** The per-user, per-app, and admin
|
||||||
|
OpenBao policies granted `list` only on `secret/metadata/.../*` (nested
|
||||||
|
paths), never on the directory path itself — so listing a directory's
|
||||||
|
*contents* (which checks `list` on the directory, e.g.
|
||||||
|
`secret/metadata/users/<uid>` or the mount root `secret/metadata`) was denied.
|
||||||
|
`vault_broker.js`'s `userPolicyHcl`/`appPolicyHcl` now also grant `list` on the
|
||||||
|
bare directory path, and `ensurePolicy` now always re-writes the policy
|
||||||
|
(idempotent) so already-created `user-<uid>` policies pick up the new grant on
|
||||||
|
the next vault-page visit. The matching `sso-admin` mount-root grant ships in
|
||||||
|
theta-suite v1.31.1 (`setup.sh`), where `ensure_policy` is likewise made
|
||||||
|
always-write so re-running `./setup.sh` applies policy edits.
|
||||||
|
- **`/profile` no longer shows literal `{{…}}` tags.** Three template fragments
|
||||||
|
sat outside the `jq-repeat="user"` scope, so they rendered raw: the card
|
||||||
|
header `Profile: {{user.uid}}`, the `Members of {{user.uid}}'s Group` tab
|
||||||
|
label, and the Admin Actions block's `{{#isActive}}`/`{{#isInactive}}`
|
||||||
|
buttons. The header/label are now populated by JS (the `Members` label
|
||||||
|
already had a setter pointing at a missing id); the Admin Actions block is
|
||||||
|
moved inside the scope so `{{uid}}`/`{{#isActive}}`/`{{#isInactive}}` render
|
||||||
|
and the correct Activate/Deactivate button shows.
|
||||||
|
- **Editing a plugin now persists.** The Edit modal had been prefilled with the
|
||||||
|
masked secret values and rendered them as fields, but `PUT /:id` only saves
|
||||||
|
non-secret config — so an edited secret was silently dropped. The Edit modal
|
||||||
|
now shows **non-secret fields only** (secrets have their own Edit-Secrets
|
||||||
|
modal), removing the confusion.
|
||||||
|
- **nmap plugin: "NMAP not found at command location: nmap"** — the `nmap`
|
||||||
|
binary was not installed in the app image. `Dockerfile.openldap` now `apk
|
||||||
|
add`s `nmap` in the runtime stage, and `plugins/discovery/nmap.js` translates
|
||||||
|
the opaque node-nmap spawn-missing error into an actionable `lastError`.
|
||||||
|
|
||||||
|
##### Added
|
||||||
|
- **SMS (VoIP.ms) configuration on `/conf`.** The existing VoIP.ms SMS sender
|
||||||
|
(`models/sms.js`, used for 2FA OTP delivery) was configurable only via env /
|
||||||
|
config files. It now has an SMS card on `/conf` (API username, DID, API
|
||||||
|
password), saved to OpenBao at `secret/sso-manager/conf` under `voipms`, with
|
||||||
|
the API password masked (`********`) and leave-blank-to-keep — mirroring the
|
||||||
|
SMTP card exactly. `models/sms.js` reads `conf.voipms.*` at call time, so a
|
||||||
|
saved change takes effect live without a restart.
|
||||||
|
- **Terms of Service editor moved to `/conf`** from the admin Overview
|
||||||
|
dashboard, where it never belonged. The same `app.tos.get`/`update` flow,
|
||||||
|
the "require all users to re-accept" checkbox, and the `app_sso_admin` gate
|
||||||
|
(matching `routes/tos.js`'s PUT gate) are preserved. The Overview page keeps
|
||||||
|
stats, notifications, and metrics.
|
||||||
|
|
||||||
|
## [v1.31.0] - 2026-08-01
|
||||||
|
|
||||||
|
Roll-up release: bumps the composed submodules to their latest tags so a fresh
|
||||||
|
`git clone` + `./setup.sh` deploys the SSO Manager plugin system, the `/conf`
|
||||||
|
SMTP/OAuth secret masking, and the ldap-client changelog. `proxy` (v1.13.1) and
|
||||||
|
`jump-host` (v1.14.1) were already at latest and are unchanged.
|
||||||
|
|
||||||
|
### Changed (submodule gitlinks)
|
||||||
|
- **sso-manager-node**: `v1.16.1` → `v1.17.1` (the plugin system shipped in
|
||||||
|
v1.17.0, plus the v1.17.1 `/conf` secret-masking hardening).
|
||||||
|
- **ldap-client**: `v1.1.1` → `v1.23.0` — a CHANGELOG-only release (the new
|
||||||
|
`CHANGELOG.md` documenting v1.1.0/v1.0.0; **no code change** — the "UI polish"
|
||||||
|
tag message is misleading, the v1.1.1…v1.23.0 diff is `CHANGELOG.md` only).
|
||||||
|
|
||||||
|
### Deploy
|
||||||
|
Operators upgrading from a prior release:
|
||||||
|
1. `git pull` and `git submodule update --init --recursive` (or a fresh clone).
|
||||||
|
2. Re-run `./setup.sh` — this is **required** if you haven't yet applied the
|
||||||
|
v1.30.1 `sso-broker` OpenBao policy grant for `secret/plugins/*` (idempotent;
|
||||||
|
it grants the existing `SSO_VAULT_TOKEN` access live, so plugin-secrets
|
||||||
|
storage works).
|
||||||
|
3. `docker compose build && docker compose up -d`. Existing
|
||||||
|
`conf.discovery.plugins` setups auto-migrate into `PluginInstance` rows +
|
||||||
|
OpenBao secrets on first boot of sso v1.17.x.
|
||||||
|
|
||||||
|
### Bundled submodule release notes
|
||||||
|
|
||||||
|
#### sso-manager-node v1.17.0 — real plugin system (loadable instances + OpenBao secrets)
|
||||||
|
|
||||||
|
## [1.17.0] - 2026-08-01
|
||||||
|
|
||||||
|
A real **plugin system**: the half-built discovery plugins (statically
|
||||||
|
configured in `sso-secrets.js`, only toggleable for cron/enabled) become
|
||||||
|
**configurable, loadable/unloadable plugin instances** you manage from a
|
||||||
|
dedicated **Plugins** page and the `/api/plugins` API, with multiple runtime
|
||||||
|
copies of each type and per-instance secrets stored in OpenBao.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Plugin instances** — a new `PluginInstance` ORM model
|
||||||
|
(`nodejs/models/plugin_instance.js`, Sequelize) is the registry of
|
||||||
|
configured, scheduled plugin copies. Each has a `pluginType`, a unique
|
||||||
|
`slug` (the discovery source name), a cron schedule, an `enabled` flag
|
||||||
|
(load/unload), non-secret `config` (JSON), and last-run bookkeeping. Multiple
|
||||||
|
instances of the same type are supported.
|
||||||
|
- **Plugin registry** (`nodejs/services/plugin_registry.js`) — generalizes the
|
||||||
|
one-shot discovery-plugin scan in `scheduler.js`. Plugin types are modules
|
||||||
|
under `nodejs/plugins/<category>/<type>.js` exporting a manifest
|
||||||
|
(`type`, `category`, `name`, `description`, `configSchema`, `validate`,
|
||||||
|
`run`/`discover`). Exposes `getTypes`, `getModule`, `splitConfig` (secret vs
|
||||||
|
non-secret), `mask`, and required-field helpers for the UI/API.
|
||||||
|
- **Per-instance secrets in OpenBao** (`nodejs/utils/plugin_secrets.js`) —
|
||||||
|
`configSchema` fields flagged `secret:true` (e.g. a Proxmox `tokenSecret`,
|
||||||
|
UniFi `password`) are stored at `secret/plugins/<instance-id>/conf`, never in
|
||||||
|
the DB. The UI only ever sees masked (`********`) values. Plugins run
|
||||||
|
in-process (BullMQ workers), so they need no OpenBao token of their own — the
|
||||||
|
SSO reads/writes via the `sso-broker` token. **Requires theta-suite ≥ v1.30.1**
|
||||||
|
for the `sso-broker` policy grant on `secret/plugins/*`; the API fails-soft
|
||||||
|
with a clear error if absent.
|
||||||
|
- **`/api/plugins` API** (`nodejs/routes/api_plugins.js`, replaces the old
|
||||||
|
`routes/plugins.js`) — `GET /types`, list/get/create/update/update-secrets/
|
||||||
|
test/load/unload/run/delete/runs. Admin-only
|
||||||
|
(`app_sso_admin` / `app_sso_directory_admin` / `app_super_admin`).
|
||||||
|
- **Plugins page** (`/plugins`, `views/plugins.ejs`) + nav entry — instance
|
||||||
|
table with New/Edit/Edit-Secrets/Test/Run-now/Load/Unload/Delete, config forms
|
||||||
|
rendered from each type's `configSchema`.
|
||||||
|
- **`validate`** ("Test" button) on the built-in Proxmox/UniFi/Nmap plugins.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `services/scheduler.js` now schedules from the `PluginInstance` table instead
|
||||||
|
of static `conf.discovery.plugins` + a Redis override hash. Each instance owns
|
||||||
|
a stable BullMQ JobScheduler id (`plugin:<instanceId>`) so load/unload
|
||||||
|
upsert/remove one schedule without disturbing the rest. Discovery plugins
|
||||||
|
reconcile results under the instance's `slug`.
|
||||||
|
- The three discovery plugins (`plugins/discovery/{proxmox,unifi,nmap}.js`)
|
||||||
|
gained manifests (`configSchema`, `validate`, `run` alias). `nmap`'s
|
||||||
|
`targetRange` is non-secret; Proxmox `tokenSecret` and UniFi `password` are
|
||||||
|
secret.
|
||||||
|
- The `/plugins` page route renders the page instead of redirecting to
|
||||||
|
`/directory`; the **Agents & Scheduler** tab was removed from `/directory`
|
||||||
|
(plugins are now managed on the Plugins page). The `/docs/agents` link is
|
||||||
|
aliased to `/docs/plugins`.
|
||||||
|
- `docs/plugins.md`, `docs/vault.md`, `docs/_config.yml` (nav), and `API.md`
|
||||||
|
(Plugin Endpoints section) document the new system.
|
||||||
|
|
||||||
|
### Legacy migration
|
||||||
|
On first boot of v1.17.0, if the `PluginInstance` table is empty **and**
|
||||||
|
`conf.discovery.plugins` has entries, one instance per configured type is seeded
|
||||||
|
automatically (secret fields copied into OpenBao). After that the static
|
||||||
|
config is ignored — manage plugins from the UI/API. Idempotent (guarded by the
|
||||||
|
empty-table check).
|
||||||
|
|
||||||
|
### Prerequisite
|
||||||
|
**theta-suite ≥ v1.30.1** — re-run `./setup.sh` after upgrading so the
|
||||||
|
`sso-broker` OpenBao policy is granted `secret/plugins/*`. Without it, storing
|
||||||
|
plugin secrets fails with a clear error.
|
||||||
|
|
||||||
|
|
||||||
|
#### sso-manager-node v1.17.1 — mask SMTP/OAuth secrets + leave-blank-to-keep on /conf
|
||||||
|
|
||||||
|
## [1.17.1] - 2026-08-01
|
||||||
|
|
||||||
|
Hardens the **runtime SMTP/OAuth secret handling** on the `/conf` admin page to
|
||||||
|
match the plugin-secrets discipline: the SMTP password and OAuth JWT secret are
|
||||||
|
no longer returned in cleartext by `GET /api/conf` or round-tripped through the
|
||||||
|
form. They remain saved in OpenBao at `secret/sso-manager/conf` at runtime
|
||||||
|
(unchanged) — only how they're surfaced to the admin changes.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **`GET /api/conf`** now masks `smtp.pass` and `oauth.jwtSecret` to `********`
|
||||||
|
(was: returned in cleartext). Non-secret fields (host, port, user, from,
|
||||||
|
secure, issuer, token lifetimes) are returned as before.
|
||||||
|
- **`POST /api/conf`** now treats a blank or `********` secret-field submission
|
||||||
|
as "keep the current stored value" — so an admin editing the From address or
|
||||||
|
token lifetimes no longer has to re-enter (or leak) the SMTP password / JWT
|
||||||
|
secret. Only a genuinely new, non-blank value overwrites. The preserved values
|
||||||
|
are re-applied to live `conf` immediately, as before.
|
||||||
|
- **`/conf` page** (`views/conf.ejs`): the Password and JWT Secret fields carry
|
||||||
|
a "leave unchanged to keep the current value stored in OpenBao" hint; the page
|
||||||
|
copy notes secret fields are masked. No JSON-textarea editing is involved —
|
||||||
|
SMTP is and remains configured through structured form fields.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- SMTP (and OAuth) config was **already** saved to OpenBao at runtime before
|
||||||
|
this release (via `POST /api/conf` → `baoConf.set('sso-manager/conf')`, and
|
||||||
|
overlaid back at boot by `bao-conf.init`). This release closes the
|
||||||
|
cleartext-exposure gap; it does not move the storage path.
|
||||||
|
- No theta-suite policy change required — `secret/sso-manager/conf` was already
|
||||||
|
granted to the `sso-broker` policy.
|
||||||
|
|
||||||
|
|
||||||
|
#### ldap-client v1.23.0 — CHANGELOG-only (no code change)
|
||||||
|
|
||||||
|
Adds a `CHANGELOG.md` documenting v1.1.0 (`app_super_admin` / `app_jump_admin`
|
||||||
|
group support in SSSD access filters; the sso/jump-host TLS-validation
|
||||||
|
divergence) and v1.0.0 (initial SSSD LDAP auth release). No source changes vs
|
||||||
|
v1.1.1; the v1.23.0 tag commit only adds this file.
|
||||||
|
|
||||||
|
## [v1.30.1] - 2026-08-01
|
||||||
|
|
||||||
|
Prerequisite release for the SSO Manager plugin system (shipped in
|
||||||
|
sso-manager-node v1.17.0). Grants the `sso-broker` OpenBao policy access to the
|
||||||
|
new per-instance plugin secrets namespace so the SSO can store plugin secrets in
|
||||||
|
OpenBao instead of `sso-secrets.js`.
|
||||||
|
|
||||||
|
### Changed (theta-suite orchestration)
|
||||||
|
- **`setup.sh`**: added `secret/data/plugins/*` (CRUD+list) and
|
||||||
|
`secret/metadata/plugins/*` (list/read/delete) to the `sso-broker` policy
|
||||||
|
HCL. `ensure_policy sso-broker` is idempotent, so re-running `./setup.sh`
|
||||||
|
immediately grants the existing `SSO_VAULT_TOKEN` access to `secret/plugins/*`
|
||||||
|
(policies are evaluated live; the token keeps its id). The SSO side fails-soft
|
||||||
|
with a clear error if this grant is absent.
|
||||||
|
- **Docs**: `docs/secrets.md` (new "Plugin secrets" section + `sso-broker`
|
||||||
|
policy row) and `docs/architecture.md` (sso-manager access row) now list
|
||||||
|
`secret/plugins/*`.
|
||||||
|
|
||||||
|
> The plugin system itself (configurable plugin instances, load/unload, UI/API,
|
||||||
|
> multi-copy, secrets in OpenBao) is in sso-manager-node v1.17.0; theta-suite
|
||||||
|
> will bump its submodule gitlink to that release next.
|
||||||
|
|
||||||
## [v1.30.0] - 2026-08-01
|
## [v1.30.0] - 2026-08-01
|
||||||
|
|
||||||
The project is renamed **theta-env → theta-suite** — it has grown from a
|
The project is renamed **theta-env → theta-suite** — it has grown from a
|
||||||
|
|||||||
+19
-1
@@ -99,7 +99,7 @@ the paths it needs:
|
|||||||
|
|
||||||
| Service | env var | Policy | Access |
|
| Service | env var | Policy | Access |
|
||||||
|---------|---------|--------|--------|
|
|---------|---------|--------|--------|
|
||||||
| sso-manager | `SSO_VAULT_TOKEN` | `sso-broker` | `secret/sso-manager/conf`, `secret/users/*`, `secret/apps/*`; also mints per-user + per-app tokens |
|
| sso-manager | `SSO_VAULT_TOKEN` | `sso-broker` | `secret/sso-manager/conf`, `secret/users/*`, `secret/apps/*`, `secret/plugins/*`; also mints per-user + per-app tokens |
|
||||||
| proxy | `PROXY_VAULT_TOKEN` | `proxy` | `secret/proxy/conf` (read) |
|
| proxy | `PROXY_VAULT_TOKEN` | `proxy` | `secret/proxy/conf` (read) |
|
||||||
| jump-host | `JUMP_VAULT_TOKEN` | `jump-host` | `secret/jump-host/conf` (read) |
|
| jump-host | `JUMP_VAULT_TOKEN` | `jump-host` | `secret/jump-host/conf` (read) |
|
||||||
|
|
||||||
@@ -253,4 +253,22 @@ clone). Quick LDAP backup:
|
|||||||
docker compose exec sso-manager slapcat -f /etc/openldap/slapd.conf -b "<base>" > backup.ldif
|
docker compose exec sso-manager slapcat -f /etc/openldap/slapd.conf -b "<base>" > backup.ldif
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plugin Ecosystem
|
||||||
|
|
||||||
|
The SSO Manager utilizes a dynamic plugin registry (`nodejs/services/plugin_registry.js`) that automatically loads any `.js` file placed in the `nodejs/plugins/<category>` folders.
|
||||||
|
|
||||||
|
### Discovery Plugins
|
||||||
|
Discovery plugins (e.g., `nmap.js`, `proxmox.js`, `docker.js`) run on a defined cron schedule to sync external assets into the centralized directory catalog.
|
||||||
|
|
||||||
|
### Messaging Plugins
|
||||||
|
Messaging plugins (e.g., `twilio.js`, `webhook.js`) provide on-demand delivery capabilities for alerts, 2FA tokens, and notifications.
|
||||||
|
- **Universal REST Webhook:** Sends custom JSON payloads to platforms like Slack, Teams, or custom API endpoints securely.
|
||||||
|
- *Discord Example:* To send alerts to a Discord channel, create a new plugin instance of type "Universal REST Webhook". Set the **Webhook URL** to your Discord webhook URL (e.g., `https://discord.com/api/webhooks/...`), the **HTTP Method** to `POST`, and the **Payload Template** to `{"content": "Alert for {{to}}: {{message}}"}`. Leave the Headers and API Secret blank.
|
||||||
|
- **Twilio SMS:** Sends standard SMS codes.
|
||||||
|
- **Fallback:** If no messaging plugins are enabled, the system falls back to the legacy `voipms` integration configured in the SSO secrets.
|
||||||
|
|
||||||
|
Secrets belonging to plugins are automatically pushed to OpenBao (`secret/plugins/<id>/conf`) and are never written to the local database, following the global secrets architecture.
|
||||||
|
|
||||||
[← Back to Home](index.html)
|
[← Back to Home](index.html)
|
||||||
+28
-2
@@ -60,7 +60,7 @@ never passed to a service container.
|
|||||||
|
|
||||||
| Policy | Capabilities | Held by |
|
| Policy | Capabilities | Held by |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `sso-broker` | read/write `secret/sso-manager/conf`, `secret/users/*`, `secret/apps/*`; `update` on `auth/token/create/sso-broker`; `update` on `sys/policies/acl/user-*`, `app-*`, `sso-admin` | SSO (`SSO_VAULT_TOKEN`) |
|
| `sso-broker` | read/write `secret/sso-manager/conf`, `secret/users/*`, `secret/apps/*`, `secret/plugins/*`; `update` on `auth/token/create/sso-broker`; `update` on `sys/policies/acl/user-*`, `app-*`, `sso-admin` | SSO (`SSO_VAULT_TOKEN`) |
|
||||||
| `sso-admin` | read/write/list all of `secret/*` | admin UI sessions (minted by the broker) |
|
| `sso-admin` | read/write/list all of `secret/*` | admin UI sessions (minted by the broker) |
|
||||||
| `proxy` | read `secret/proxy/conf` | proxy (`PROXY_VAULT_TOKEN`) |
|
| `proxy` | read `secret/proxy/conf` | proxy (`PROXY_VAULT_TOKEN`) |
|
||||||
| `jump-host` | read `secret/jump-host/conf` | jump host (`JUMP_VAULT_TOKEN`) |
|
| `jump-host` | read `secret/jump-host/conf` | jump host (`JUMP_VAULT_TOKEN`) |
|
||||||
@@ -150,6 +150,31 @@ const data = await baoConf.get('apps/my-service/conf'); // secret/data/apps/my-s
|
|||||||
await baoConf.set('apps/my-service/conf', { db_password: '...' });
|
await baoConf.set('apps/my-service/conf', { db_password: '...' });
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Plugin secrets
|
||||||
|
|
||||||
|
The SSO Manager's plugin system (configurable plugin instances you create,
|
||||||
|
edit, load/unload, and run from the **Plugins** page) stores each instance's
|
||||||
|
secrets in its own OpenBao namespace, `secret/plugins/<instance-id>/conf`,
|
||||||
|
rather than in the static `sso-secrets.js` `discovery.plugins` block. The
|
||||||
|
SSO reads and writes these server-side through the `sso-broker` token (the
|
||||||
|
plugin runs in-process as a BullMQ worker, so it needs no token of its own),
|
||||||
|
and the admin UI only ever sees masked (`********`) values.
|
||||||
|
|
||||||
|
- A **plugin type** is a module under `nodejs/plugins/<category>/<type>.js`
|
||||||
|
exporting a manifest (`configSchema` declares which fields are `secret`).
|
||||||
|
- A **plugin instance** is a configured, loadable/unloadable copy of a type,
|
||||||
|
tracked in the `PluginInstance` table; you can have multiple instances of the
|
||||||
|
same type (e.g. two Proxmox endpoints with their own tokens).
|
||||||
|
- Non-secret config lives in the DB row; only the `secret:true` field values
|
||||||
|
live in `secret/plugins/<instance-id>/conf`.
|
||||||
|
|
||||||
|
Deleting an instance removes both the DB row and its `secret/plugins/<id>/*`
|
||||||
|
namespace. Legacy `discovery.plugins` entries in `sso-secrets.js` are migrated
|
||||||
|
to instances automatically on the first boot of SSO Manager ≥ v1.17.0 (the
|
||||||
|
secret fields are copied into OpenBao at that point). See the SSO Manager
|
||||||
|
[plugins docs](https://theta42.github.io/sso-manager-node/plugins.html) for the
|
||||||
|
UI/API reference.
|
||||||
|
|
||||||
## Operator rotation
|
## Operator rotation
|
||||||
|
|
||||||
If a secret is exposed (or just on a routine schedule), rotate it at the
|
If a secret is exposed (or just on a routine schedule), rotate it at the
|
||||||
@@ -192,4 +217,5 @@ re-mint the per-app tokens.
|
|||||||
git-destructive operation you can opt into.
|
git-destructive operation you can opt into.
|
||||||
- **Per-app secrets beyond boot config** (e.g. the proxy's DNS-provider creds,
|
- **Per-app secrets beyond boot config** (e.g. the proxy's DNS-provider creds,
|
||||||
the jump host's per-user LDAP SSH keys) moving into OpenBao — only the
|
the jump host's per-user LDAP SSH keys) moving into OpenBao — only the
|
||||||
boot-critical `*-secrets.js` contents moved in this phase.
|
boot-critical `*-secrets.js` contents moved in this phase. (Plugin instance
|
||||||
|
secrets *are* in OpenBao, at `secret/plugins/<id>/conf` — see above.)
|
||||||
+1
-1
Submodule ldap-client updated: 31d8fa1229...ca8132f5ff
Executable
+458
@@ -0,0 +1,458 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# LDAP Migration Script for theta42
|
||||||
|
#
|
||||||
|
# Migrates an existing OpenLDAP server to the theta42 stack.
|
||||||
|
# Exports data from source, transforms as needed, imports into theta42.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./migrate-ldap.sh --source-host <ldap-uri> --source-bind-dn <dn> --source-bind-pass <pass> --target-domain <domain>
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# ./migrate-ldap.sh --source-host ldap://192.168.1.10:389 --source-bind-dn "cn=admin,dc=example,dc=com" --source-bind-pass "secret" --target-domain "example.com"
|
||||||
|
#
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# ── Defaults ──────────────────────────────────────────────────────────────────
|
||||||
|
SOURCE_HOST=""
|
||||||
|
SOURCE_BIND_DN=""
|
||||||
|
SOURCE_BIND_PASS=""
|
||||||
|
TARGET_DOMAIN=""
|
||||||
|
BASE_DN=""
|
||||||
|
EXPORT_DIR="./ldap-migration-$(date +%Y%m%d-%H%M%S)"
|
||||||
|
THETA_ENV_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
# ── Colors ────────────────────────────────────────────────────────────────────
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
info() { printf "${BLUE}[migrate]${NC} %s\n" "$*"; }
|
||||||
|
warn() { printf "${YELLOW}[migrate]${NC} %s\n" "$*" >&2; }
|
||||||
|
error() { printf "${RED}[migrate]${NC} %s\n" "$*" >&2; }
|
||||||
|
success() { printf "${GREEN}[migrate]${NC} %s\n" "$*" >&2; }
|
||||||
|
die() { error "$*"; exit 1; }
|
||||||
|
|
||||||
|
# ── Argument parsing ──────────────────────────────────────────────────────────
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--source-host)
|
||||||
|
SOURCE_HOST="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--source-bind-dn)
|
||||||
|
SOURCE_BIND_DN="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--source-bind-pass)
|
||||||
|
SOURCE_BIND_PASS="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--target-domain)
|
||||||
|
TARGET_DOMAIN="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--export-dir)
|
||||||
|
EXPORT_DIR="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
cat <<EOF
|
||||||
|
LDAP Migration Script for theta42
|
||||||
|
|
||||||
|
Usage: $0 --source-host <uri> --source-bind-dn <dn> --source-bind-pass <pass> --target-domain <domain>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--source-host Source LDAP URI (e.g., ldap://192.168.1.10:389 or ldaps://ldap.example.com:636)
|
||||||
|
--source-bind-dn Bind DN for source LDAP (e.g., cn=admin,dc=example,dc=com)
|
||||||
|
--source-bind-pass Bind password for source LDAP
|
||||||
|
--target-domain Target domain for theta42 (e.g., example.com)
|
||||||
|
--export-dir Directory for exports (default: ./ldap-migration-<timestamp>)
|
||||||
|
--help Show this help message
|
||||||
|
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "Unknown option: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Validation ────────────────────────────────────────────────────────────────
|
||||||
|
[[ -n "$SOURCE_HOST" ]] || die "Missing --source-host"
|
||||||
|
[[ -n "$SOURCE_BIND_DN" ]] || die "Missing --source-bind-dn"
|
||||||
|
[[ -n "$SOURCE_BIND_PASS" ]] || die "Missing --source-bind-pass"
|
||||||
|
[[ -n "$TARGET_DOMAIN" ]] || die "Missing --target-domain"
|
||||||
|
|
||||||
|
# Derive base DN from domain (e.g., example.com -> dc=example,dc=com)
|
||||||
|
BASE_DN="$(echo "$TARGET_DOMAIN" | sed 's/\./,dc=/g; s/^/dc=/')"
|
||||||
|
|
||||||
|
info "Migration configuration:"
|
||||||
|
info " Source host: $SOURCE_HOST"
|
||||||
|
info " Source bind DN: $SOURCE_BIND_DN"
|
||||||
|
info " Target domain: $TARGET_DOMAIN"
|
||||||
|
info " Target base DN: $BASE_DN"
|
||||||
|
info " Export dir: $EXPORT_DIR"
|
||||||
|
|
||||||
|
# ── Prerequisites ─────────────────────────────────────────────────────────────
|
||||||
|
command -v ldapsearch >/dev/null 2>&1 || die "ldapsearch not found. Install ldap-utils."
|
||||||
|
command -v slapcat >/dev/null 2>&1 || die "slapcat not found."
|
||||||
|
command -v docker >/dev/null 2>&1 || die "docker not found."
|
||||||
|
command -v docker-compose >/dev/null 2>&1 || command -v docker compose >/dev/null 2>&1 || die "docker compose not found."
|
||||||
|
|
||||||
|
if [[ -d "$EXPORT_DIR" ]]; then
|
||||||
|
warn "Export directory already exists: $EXPORT_DIR"
|
||||||
|
read -p "Overwrite? [y/N] " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
info "Aborted."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
mkdir -p "$EXPORT_DIR"
|
||||||
|
|
||||||
|
# ── Phase 1: Export from source LDAP ─────────────────────────────────────────
|
||||||
|
info "Phase 1: Exporting data from source LDAP..."
|
||||||
|
|
||||||
|
# Export each subtree
|
||||||
|
export_subtree() {
|
||||||
|
local base="$1"
|
||||||
|
local outfile="$2"
|
||||||
|
info " Exporting $base -> $outfile"
|
||||||
|
|
||||||
|
# Use ldapsearch with -LLL for LDIF output
|
||||||
|
if ! ldapsearch -x -H "$SOURCE_HOST" -D "$SOURCE_BIND_DN" -w "$SOURCE_BIND_PASS" \
|
||||||
|
-b "$base" -s sub "(objectClass=*)" > "$outfile" 2>/dev/null; then
|
||||||
|
warn " No data or base DN not found: $base"
|
||||||
|
# Create empty file to signal "checked"
|
||||||
|
echo "# No data for $base" > "$outfile"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export standard subtrees
|
||||||
|
export_subtree "ou=people,$BASE_DN" "$EXPORT_DIR/01-people.ldif"
|
||||||
|
export_subtree "ou=groups,$BASE_DN" "$EXPORT_DIR/02-groups.ldif"
|
||||||
|
export_subtree "ou=sudoers,$BASE_DN" "$EXPORT_DIR/03-sudoers.ldif"
|
||||||
|
export_subtree "ou=services,$BASE_DN" "$EXPORT_DIR/04-services.ldif"
|
||||||
|
|
||||||
|
# Also export cn=config for reference (read-only, won't import)
|
||||||
|
info " Exporting cn=config for reference..."
|
||||||
|
ldapsearch -x -H "$SOURCE_HOST" -D "$SOURCE_BIND_DN" -w "$SOURCE_BIND_PASS" \
|
||||||
|
-b "cn=config" -s sub "(objectClass=*)" > "$EXPORT_DIR/00-config-reference.ldif" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Count entries
|
||||||
|
for f in "$EXPORT_DIR"/*.ldif; do
|
||||||
|
count=$(grep -c "^dn:" "$f" 2>/dev/null || echo 0)
|
||||||
|
info " $(basename "$f"): $count entries"
|
||||||
|
done
|
||||||
|
|
||||||
|
success "Export complete: $EXPORT_DIR"
|
||||||
|
|
||||||
|
# ── Phase 2: Transform LDIF ──────────────────────────────────────────────────
|
||||||
|
info "Phase 2: Transforming LDIF for theta42 compatibility..."
|
||||||
|
|
||||||
|
# Create transformation script
|
||||||
|
cat > "$EXPORT_DIR/transform.sh" <<'TRANSFORM_SCRIPT'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Transform exported LDIF for theta42 compatibility
|
||||||
|
|
||||||
|
INPUT="$1"
|
||||||
|
OUTPUT="$2"
|
||||||
|
BASE_DN="$3"
|
||||||
|
|
||||||
|
# theta42 requires certain objectClasses and attributes
|
||||||
|
# This script:
|
||||||
|
# 1. Ensures posixAccount has uidNumber, gidNumber, homeDirectory, loginShell
|
||||||
|
# 2. Ensures groupOfNames has at least one member
|
||||||
|
# 3. Adds ldapPublicKey objectClass where sshPublicKey exists
|
||||||
|
# 4. Normalizes password hash formats if needed
|
||||||
|
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
echo "$line"
|
||||||
|
done < "$INPUT" > "$OUTPUT"
|
||||||
|
|
||||||
|
echo "Transform complete: $OUTPUT"
|
||||||
|
TRANSFORM_SCRIPT
|
||||||
|
chmod +x "$EXPORT_DIR/transform.sh"
|
||||||
|
|
||||||
|
# For now, we'll do a direct import. The transformation is minimal for most setups.
|
||||||
|
# If you have custom schemas, you may need to edit the LDIF manually.
|
||||||
|
|
||||||
|
# ── Phase 3: Prepare theta42 LDAP ────────────────────────────────────────────
|
||||||
|
info "Phase 3: Preparing theta42 LDAP..."
|
||||||
|
|
||||||
|
# Stop theta42 stack
|
||||||
|
COMPOSE_CMD=""
|
||||||
|
if docker compose version >/dev/null 2>&1; then
|
||||||
|
COMPOSE_CMD="docker compose"
|
||||||
|
elif command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
COMPOSE_CMD="docker-compose"
|
||||||
|
else
|
||||||
|
die "docker compose not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info " Stopping sso-manager container..."
|
||||||
|
$COMPOSE_CMD stop sso-manager 2>/dev/null || true
|
||||||
|
|
||||||
|
# Wait for container to stop
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# ── Phase 4: Import into theta42 ─────────────────────────────────────────────
|
||||||
|
info "Phase 4: Importing data into theta42 LDAP..."
|
||||||
|
|
||||||
|
# Create import script that runs inside the container
|
||||||
|
cat > "$EXPORT_DIR/import-to-theta42.sh" <<'IMPORT_SCRIPT'
|
||||||
|
#!/bin/bash
|
||||||
|
# Run inside theta42 sso-manager container to import LDIF
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
EXPORT_DIR="$1"
|
||||||
|
BASE_DN="$2"
|
||||||
|
|
||||||
|
# Stop slapd if running
|
||||||
|
pkill slapd 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Clear existing data (but preserve structure)
|
||||||
|
info "Clearing existing LDAP data..."
|
||||||
|
rm -rf /var/lib/ldap/*
|
||||||
|
rm -rf /var/lib/ldap/db.*
|
||||||
|
|
||||||
|
# Initialize LDAP database with theta42 schema
|
||||||
|
info "Initializing LDAP database..."
|
||||||
|
|
||||||
|
# Create initial LDIF with base structure
|
||||||
|
cat > /tmp/base.ldif <<EOF
|
||||||
|
dn: $BASE_DN
|
||||||
|
objectClass: top
|
||||||
|
objectClass: dcObject
|
||||||
|
objectClass: organization
|
||||||
|
dc: $(echo $BASE_DN | sed 's/,dc=.*//; s/dc=//')
|
||||||
|
o: Organization
|
||||||
|
|
||||||
|
dn: ou=people,$BASE_DN
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
ou: people
|
||||||
|
|
||||||
|
dn: ou=groups,$BASE_DN
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
ou: groups
|
||||||
|
|
||||||
|
dn: ou=sudoers,$BASE_DN
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
ou: sudoers
|
||||||
|
|
||||||
|
dn: ou=services,$BASE_DN
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
ou: services
|
||||||
|
|
||||||
|
dn: cn=admin,$BASE_DN
|
||||||
|
objectClass: organizationalRole
|
||||||
|
cn: admin
|
||||||
|
description: LDAP Administrator
|
||||||
|
|
||||||
|
dn: cn=ldap-admin,ou=groups,$BASE_DN
|
||||||
|
objectClass: groupOfNames
|
||||||
|
cn: ldap-admin
|
||||||
|
member: cn=admin,$BASE_DN
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Import base structure
|
||||||
|
slapadd -c -l /tmp/base.ldif -b "$BASE_DN" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Import user data
|
||||||
|
for f in "$EXPORT_DIR"/*.ldif; do
|
||||||
|
[[ -f "$f" ]] || continue
|
||||||
|
[[ "$(basename "$f")" == "00-config-reference.ldif" ]] && continue
|
||||||
|
|
||||||
|
info "Importing $f..."
|
||||||
|
# Use -c to continue on errors (some entries may already exist)
|
||||||
|
slapadd -c -l "$f" -b "$BASE_DN" 2>/dev/null || warn "Some entries in $f may have failed"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Fix ownership
|
||||||
|
chown -R ldap:ldap /var/lib/ldap
|
||||||
|
|
||||||
|
# Start slapd
|
||||||
|
info "Starting slapd..."
|
||||||
|
exec /usr/sbin/slapd -h "ldap:/// ldaps:///" -u ldap -g ldap
|
||||||
|
|
||||||
|
IMPORT_SCRIPT
|
||||||
|
|
||||||
|
# Copy import script to export dir
|
||||||
|
cp "$EXPORT_DIR/import-to-theta42.sh" "$EXPORT_DIR/"
|
||||||
|
|
||||||
|
# Run the import inside the container
|
||||||
|
info "Running import inside sso-manager container..."
|
||||||
|
|
||||||
|
# First, start a temporary container to do the import
|
||||||
|
$COMPOSE_CMD up -d sso-manager 2>/dev/null || true
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Copy LDIF files into container
|
||||||
|
info "Copying LDIF files to container..."
|
||||||
|
for f in "$EXPORT_DIR"/*.ldif; do
|
||||||
|
[[ -f "$f" ]] || continue
|
||||||
|
docker cp "$f" sso-manager:/tmp/migration/ 2>/dev/null || {
|
||||||
|
docker exec sso-manager mkdir -p /tmp/migration
|
||||||
|
docker cp "$f" sso-manager:/tmp/migration/
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
# Run import
|
||||||
|
info "Executing import..."
|
||||||
|
docker exec sso-manager bash -c "
|
||||||
|
pkill slapd 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Clear data
|
||||||
|
rm -rf /var/lib/ldap/*
|
||||||
|
|
||||||
|
# Create base structure
|
||||||
|
slapadd -c -b '$BASE_DN' <<EOF
|
||||||
|
dn: $BASE_DN
|
||||||
|
objectClass: top
|
||||||
|
objectClass: dcObject
|
||||||
|
objectClass: organization
|
||||||
|
dc: $(echo $BASE_DN | cut -d',' -f1 | cut -d'=' -f2)
|
||||||
|
o: $TARGET_DOMAIN
|
||||||
|
|
||||||
|
dn: ou=people,$BASE_DN
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
ou: people
|
||||||
|
|
||||||
|
dn: ou=groups,$BASE_DN
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
ou: groups
|
||||||
|
|
||||||
|
dn: ou=sudoers,$BASE_DN
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
ou: sudoers
|
||||||
|
|
||||||
|
dn: ou=services,$BASE_DN
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
ou: services
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Import user data
|
||||||
|
for f in /tmp/migration/*.ldif; do
|
||||||
|
[[ \"\$(basename \$f)\" == \"00-config-reference.ldif\" ]] && continue
|
||||||
|
[[ -f \"\$f\" ]] || continue
|
||||||
|
echo \"Importing \$f...\"
|
||||||
|
slapadd -c -l \"\$f\" -b '$BASE_DN' 2>/dev/null || echo \"Warning: Some entries in \$f may have failed\"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Fix ownership
|
||||||
|
chown -R ldap:ldap /var/lib/ldap
|
||||||
|
|
||||||
|
echo \"Import complete!\"
|
||||||
|
" || warn "Import had some errors - check output above"
|
||||||
|
|
||||||
|
# ── Phase 5: Create theta42 admin groups ─────────────────────────────────────
|
||||||
|
info "Phase 5: Creating theta42 admin groups..."
|
||||||
|
|
||||||
|
# Create LDIF for theta42-specific groups
|
||||||
|
cat > "$EXPORT_DIR/theta42-groups.ldif" <<EOF
|
||||||
|
# theta42 administrative groups
|
||||||
|
# These groups control access to various features
|
||||||
|
|
||||||
|
# Cross-app super admin - full admin in all apps
|
||||||
|
dn: cn=app_super_admin,ou=groups,$BASE_DN
|
||||||
|
objectClass: groupOfNames
|
||||||
|
objectClass: top
|
||||||
|
cn: app_super_admin
|
||||||
|
description: Cross-app super administrators
|
||||||
|
|
||||||
|
# SSO Manager admin
|
||||||
|
dn: cn=app_sso_admin,ou=groups,$BASE_DN
|
||||||
|
objectClass: groupOfNames
|
||||||
|
objectClass: top
|
||||||
|
cn: app_sso_admin
|
||||||
|
description: SSO Manager administrators
|
||||||
|
|
||||||
|
# SSO invite - can invite users
|
||||||
|
dn: cn=app_sso_invite,ou=groups,$BASE_DN
|
||||||
|
objectClass: groupOfNames
|
||||||
|
objectClass: top
|
||||||
|
cn: app_sso_invite
|
||||||
|
description: Can send invitations
|
||||||
|
|
||||||
|
# OAuth admin - manages OAuth clients
|
||||||
|
dn: cn=app_sso_oauth_admin,ou=groups,$BASE_DN
|
||||||
|
objectClass: groupOfNames
|
||||||
|
objectClass: top
|
||||||
|
cn: app_sso_oauth_admin
|
||||||
|
description: OAuth client administrators
|
||||||
|
|
||||||
|
# Service account marker
|
||||||
|
dn: cn=app_sso_service_account,ou=groups,$BASE_DN
|
||||||
|
objectClass: groupOfNames
|
||||||
|
objectClass: top
|
||||||
|
cn: app_sso_service_account
|
||||||
|
description: Service accounts (hidden from UI)
|
||||||
|
|
||||||
|
# Jump host admin - audit access only
|
||||||
|
dn: cn=app_jump_admin,ou=groups,$BASE_DN
|
||||||
|
objectClass: groupOfNames
|
||||||
|
objectClass: top
|
||||||
|
cn: app_jump_admin
|
||||||
|
description: Jump host audit administrators
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Import the theta42 groups
|
||||||
|
docker exec sso-manager bash -c "
|
||||||
|
slapadd -c -l /tmp/theta42-groups.ldif -b '$BASE_DN' 2>/dev/null || echo \"Groups may already exist\"
|
||||||
|
" <<EOF
|
||||||
|
$(cat "$EXPORT_DIR/theta42-groups.ldif")
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ── Phase 6: Restart and verify ──────────────────────────────────────────────
|
||||||
|
info "Phase 6: Restarting theta42 stack..."
|
||||||
|
|
||||||
|
$COMPOSE_CMD restart sso-manager
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
info "Waiting for sso-manager to be healthy..."
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if docker exec sso-manager wget -q -O- http://localhost:3001/health >/dev/null 2>&1; then
|
||||||
|
success "sso-manager is healthy!"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if (( i == 30 )); then
|
||||||
|
warn "sso-manager did not become healthy in 30s. Check logs with: docker compose logs sso-manager"
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# Verify import
|
||||||
|
info "Verifying import..."
|
||||||
|
dn_count=$(docker exec sso-manager ldapsearch -x -H "ldap://localhost" -b "$BASE_DN" -s sub "(objectClass=*)" dn 2>/dev/null | grep -c "^dn:" || echo 0)
|
||||||
|
info "Total entries in LDAP: $dn_count"
|
||||||
|
|
||||||
|
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
success "Migration complete!"
|
||||||
|
echo ""
|
||||||
|
info "Summary:"
|
||||||
|
info " - Exported data saved to: $EXPORT_DIR"
|
||||||
|
info " - Base DN: $BASE_DN"
|
||||||
|
info " - Total entries: $dn_count"
|
||||||
|
echo ""
|
||||||
|
info "Next steps:"
|
||||||
|
info " 1. Review the exported LDIF files in $EXPORT_DIR"
|
||||||
|
info " 2. Add users to theta42 admin groups as needed:"
|
||||||
|
info " docker exec sso-manager ldapmodify -x -H ldap://localhost -D 'cn=admin,$BASE_DN' -w <admin-pass>"
|
||||||
|
info " 3. Update your LDAP clients to point to theta42"
|
||||||
|
info " 4. Run ./setup.sh to complete theta42 bootstrap"
|
||||||
|
echo ""
|
||||||
|
warn "IMPORTANT: Update all LDAP clients to use the new theta42 LDAP server!"
|
||||||
|
warn " - SSSD: Update /etc/sssd/sssd.conf ldap_uri"
|
||||||
|
warn " - sudo: Update /etc/sudo-ldap.conf"
|
||||||
|
warn " - Apps: Update LDAP connection strings"
|
||||||
@@ -712,12 +712,12 @@ bao_run() { docker exec -e BAO_TOKEN="$VAULT_TOKEN" openbao bao "$@"; }
|
|||||||
# Write an ACL policy from stdin HCL only if it does not already exist.
|
# Write an ACL policy from stdin HCL only if it does not already exist.
|
||||||
ensure_policy() {
|
ensure_policy() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
if bao_run policy read "$name" >/dev/null 2>&1; then
|
# Always (re)write: `bao policy write` is an idempotent overwrite, so this
|
||||||
info " policy ${name} already exists — keeping."
|
# applies policy edits on a re-run instead of stranding the old HCL
|
||||||
else
|
# forever ("already exists — keeping" silently dropped upgrades — e.g.
|
||||||
info " writing policy ${name}..."
|
# the secret/metadata mount-root list grant added for the /vault fix).
|
||||||
docker exec -i -e BAO_TOKEN="$VAULT_TOKEN" openbao bao policy write "$name" - >/dev/null
|
info " writing policy ${name}..."
|
||||||
fi
|
docker exec -i -e BAO_TOKEN="$VAULT_TOKEN" openbao bao policy write "$name" - >/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
# Read KEY= from ./.env (empty if absent) — reuse a previously minted token
|
# Read KEY= from ./.env (empty if absent) — reuse a previously minted token
|
||||||
@@ -769,7 +769,9 @@ seed_app_conf() {
|
|||||||
info "Configuring OpenBao policies..."
|
info "Configuring OpenBao policies..."
|
||||||
# sso-broker — sso's authority to read/write its own conf, mint per-user and
|
# sso-broker — sso's authority to read/write its own conf, mint per-user and
|
||||||
# per-app tokens (auth/token/create/sso-broker), and create the matching
|
# per-app tokens (auth/token/create/sso-broker), and create the matching
|
||||||
# user-<uid> / app-<name> / sso-admin policies.
|
# user-<uid> / app-<name> / sso-admin policies. secret/plugins/* holds per-instance
|
||||||
|
# plugin secrets managed by the SSO plugin system (configurable plugin copies,
|
||||||
|
# loaded/unloaded at runtime — see sso-manager-node docs/plugins.md).
|
||||||
ensure_policy sso-broker <<'HCL'
|
ensure_policy sso-broker <<'HCL'
|
||||||
path "secret/data/sso-manager/conf" { capabilities = ["create", "read", "update", "delete", "list"] }
|
path "secret/data/sso-manager/conf" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
path "secret/metadata/sso-manager/conf" { capabilities = ["list", "read", "delete"] }
|
path "secret/metadata/sso-manager/conf" { capabilities = ["list", "read", "delete"] }
|
||||||
@@ -777,14 +779,20 @@ path "secret/data/users/*" { capabilities = ["create", "read", "update", "delete
|
|||||||
path "secret/metadata/users/*" { capabilities = ["list", "read", "delete"] }
|
path "secret/metadata/users/*" { capabilities = ["list", "read", "delete"] }
|
||||||
path "secret/data/apps/*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
path "secret/data/apps/*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
path "secret/metadata/apps/*" { capabilities = ["list", "read", "delete"] }
|
path "secret/metadata/apps/*" { capabilities = ["list", "read", "delete"] }
|
||||||
|
path "secret/data/plugins/*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
|
path "secret/metadata/plugins/*" { capabilities = ["list", "read", "delete"] }
|
||||||
path "auth/token/create/sso-broker" { capabilities = ["update"] }
|
path "auth/token/create/sso-broker" { capabilities = ["update"] }
|
||||||
path "sys/policies/acl/user-*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
path "sys/policies/acl/user-*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
path "sys/policies/acl/app-*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
path "sys/policies/acl/app-*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
path "sys/policies/acl/sso-admin" { capabilities = ["create", "read", "update", "delete", "list"] }
|
path "sys/policies/acl/sso-admin" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
HCL
|
HCL
|
||||||
# sso-admin — admin users in the vault UI: read/write/list everything under secret/.
|
# sso-admin — admin users in the vault UI: read/write/list everything under secret/.
|
||||||
|
# The bare `secret/metadata` grant lets an admin LIST the KV mount root (the
|
||||||
|
# top-level dirs); `secret/metadata/*` covers nested paths but not the root
|
||||||
|
# itself, so without it the /vault secrets list 403s.
|
||||||
ensure_policy sso-admin <<'HCL'
|
ensure_policy sso-admin <<'HCL'
|
||||||
path "secret/data/*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
path "secret/data/*" { capabilities = ["create", "read", "update", "delete", "list"] }
|
||||||
|
path "secret/metadata" { capabilities = ["list", "read", "delete"] }
|
||||||
path "secret/metadata/*" { capabilities = ["list", "read", "delete"] }
|
path "secret/metadata/*" { capabilities = ["list", "read", "delete"] }
|
||||||
HCL
|
HCL
|
||||||
# proxy / jump-host — read only their own boot conf.
|
# proxy / jump-host — read only their own boot conf.
|
||||||
@@ -1044,7 +1052,7 @@ echo " Jump host (web): https://${JUMP_HOST:-jump.${SSO_HOST#sso.}} (audit
|
|||||||
echo
|
echo
|
||||||
echo " First admin login credentials are in ./config/sso-secrets.js:"
|
echo " First admin login credentials are in ./config/sso-secrets.js:"
|
||||||
echo " user: ${ADMIN_UID}"
|
echo " user: ${ADMIN_UID}"
|
||||||
echo " pass: bootstrap.adminPass"
|
echo " pass: ${CFG_ADMIN_PASS}"
|
||||||
echo
|
echo
|
||||||
echo " Proxy local admin (anti-lockout fallback if the SSO is unreachable):"
|
echo " Proxy local admin (anti-lockout fallback if the SSO is unreachable):"
|
||||||
echo " user: proxyadmin2"
|
echo " user: proxyadmin2"
|
||||||
|
|||||||
+1
-1
Submodule sso-manager-node updated: 21a56dce50...f1d52601de
@@ -0,0 +1,40 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
|
||||||
|
test('Integration Test Suite', async (t) => {
|
||||||
|
|
||||||
|
await t.test('SSO Manager should be running and healthy', async () => {
|
||||||
|
const res = await fetch('http://localhost:3001/health');
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const body = await res.json();
|
||||||
|
assert.strictEqual(body.status, 'ok');
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test('Proxy should be running and route to SSO Manager', async () => {
|
||||||
|
// Testing the proxy routes traffic to SSO manager
|
||||||
|
const res = await fetch('http://sso.localtest.me/.well-known/openid-configuration');
|
||||||
|
assert.ok(res.status === 200 || res.status === 301);
|
||||||
|
const body = await res.json();
|
||||||
|
assert.ok(body.issuer);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test('Proxy Management API should be running', async () => {
|
||||||
|
const res = await fetch('http://localhost:3000/health');
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const body = await res.json();
|
||||||
|
assert.strictEqual(body.status, 'ok');
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test('OpenBao should be running and healthy', async () => {
|
||||||
|
// Port 8080 is mapped to OpenBao's 8200 in docker-compose.yml
|
||||||
|
const res = await fetch('http://localhost:8080/v1/sys/health');
|
||||||
|
assert.ok(res.status === 200 || res.status === 501); // 501 means not initialized/sealed, but responsive
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test('SSO Manager should proxy to OpenBao (integration test)', async () => {
|
||||||
|
// Test if SSO Manager proxies to OpenBao
|
||||||
|
// Without authentication, this should return 401 Unauthorized from SSO Manager's middleware
|
||||||
|
const res = await fetch('http://localhost:3001/api/vault/sys/health');
|
||||||
|
assert.strictEqual(res.status, 401);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "theta-env-integration-tests",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Automated integration tests for theta-env projects",
|
||||||
|
"scripts": {
|
||||||
|
"test": "NODE_TLS_REJECT_UNAUTHORIZED=0 node --test *.test.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user