Compare commits

..

5 Commits

Author SHA1 Message Date
Joseph Doherty 9d373efbe0 docs(glauth): mark shared-GLAuth design implemented + all plan tasks complete 2026-06-04 16:21:13 -04:00
Joseph Doherty 4c0f1eaaf7 fix(glauth): rename OPC/Gw testers to avoid username/group case-collision
glauth exposes each group as cn=<Group> under ou=users, so a case-insensitive
(cn=x) search matched both the user and the group (2 entries -> the shared
ZB.MOM.WW.Auth.Ldap 'exactly one entry' rule failed the bind). Renamed the 4
colliding testers (readonly/writetune/alarmack/gwreader) + the 2 siblings for
consistency: opc-readonly/opc-writeop/opc-writetune/opc-writeconfig/opc-alarmack
and gw-viewer. Verified gw-viewer logs into the MxGateway dashboard as Viewer.
multi-role/admin/designer/etc. were never affected (no case-collision).
2026-06-04 16:19:33 -04:00
Joseph Doherty 0f2b2b8351 feat(glauth): merged shared dev GLAuth directory + compose + runbook (10.100.0.35)
Phase 0 of the shared-GLAuth standardization. config.toml = merged dc=zb,dc=local
directory (15 groups in partitioned 55xx/56xx/57xx families, 14 users incl.
multi-role spanning all groups, serviceaccount search account). compose runs one
glauth/glauth:latest on :3893. README is the deploy/verify runbook. Code-reviewed;
fixed scp -r idempotency in the deploy command (README + plan Task 4).
2026-06-04 15:45:41 -04:00
Joseph Doherty 5be0cec601 docs(glauth): implementation plan + tasks for shared GLAuth standardization
19 tasks across 5 phases: author scadaproj/infra/glauth/ (merged config + compose +
runbook) → deploy/verify on 10.100.0.35 (hard gate, access-prerequisite) → repoint
ScadaBridge (Mac), un-stub OtOpcUa docker-dev, repoint windev MxGateway + OtOpcUa →
retire old glauths → full cross-app verification. Co-located .tasks.json.
2026-06-04 15:37:06 -04:00
Joseph Doherty 106fb8b149 docs(glauth): shared GLAuth standardization design (dev/test consolidation onto 10.100.0.35)
Approved design: consolidate OtOpcUa, MxAccessGateway, ScadaBridge dev/test auth
onto one shared GLAuth at 10.100.0.35:3893 (dc=zb,dc=local, plaintext). App-neutral
source of truth in scadaproj/infra/glauth/; merged directory with gid families
partitioned 55xx/56xx/57xx + multi-role/admin/serviceaccount; per-app Server
repoints; incremental rollout keeping old glauths until verified.
2026-06-04 15:26:32 -04:00
6 changed files with 1053 additions and 0 deletions
@@ -0,0 +1,141 @@
# Shared GLAuth Standardization — Design
> **Status:** IMPLEMENTED + verified 2026-06-04 (all 18 plan tasks). See `shared-glauth-on-35` memory.
> Plan: [`2026-06-04-shared-glauth-standardization.md`](2026-06-04-shared-glauth-standardization.md).
> **Scope:** dev/test only. Production stays on real corporate AD (out of scope).
## Goal
Consolidate the three sister projects (OtOpcUa, MxAccessGateway, ScadaBridge) onto **one shared
GLAuth dev directory** running on the shared Docker host **`10.100.0.35:3893`**, replacing the
three separate LDAP setups in use today. This is the natural endpoint of the Auth-component
normalization: all three already use the shared `ZB.MOM.WW.Auth.Ldap` library (search-then-bind)
and already default to the same base DN `dc=zb,dc=local`.
## Decisions (locked during brainstorming)
| Decision | Choice |
|---|---|
| Environments | **Dev/test only** (prod → real AD, untouched) |
| Consolidation depth | **Full** — every dev instance points at 35 |
| Transport | **Plaintext** (`Transport=None`, `AllowInsecure=true`) — trusted lab subnet |
| Source of truth | **`scadaproj/infra/glauth/`** (app-neutral, next to the other shared `ZB.MOM.WW.*` components) — Approach A |
## Architecture
```
scadaproj/infra/glauth/ ← single source of truth (git)
├── config.toml (merged dc=zb,dc=local directory)
├── docker-compose.yml (one `glauth` service, :3893)
└── README.md
│ deploy on 10.100.0.35: docker compose up -d
GLAuth @ 10.100.0.35:3893 · datastore=config · baseDN dc=zb,dc=local · ldaps=false
▲ ▲
plaintext bind │ (None + AllowInsecure) │
┌──────────────┴───────────┐ ┌─────────┴─────────────────────┐
Mac / OrbStack │ windev (10.100.0.48)
• ScadaBridge :9000/:9100 │ • MxGateway (MxAccessGw svc)
• OtOpcUa docker-dev │ • OtOpcUa (OtOpcUa svc)
(un-stubbed)
```
- One `glauth` container on `10.100.0.35:3893`, `datastore=config`, `baseDN=dc=zb,dc=local`, ldaps disabled.
- Every dev consumer: `Server=10.100.0.35`, `Port=3893`, `Transport=None`, `AllowInsecure=true`, `SearchBase=dc=zb,dc=local`.
- **Retired:** the `scadabridge-ldap` container (ScadaBridge `infra/docker-compose.yml`) and the windev-local glauth (`C:\publish\glauth`).
- **Consequences:** windev gains a runtime dependency on 35 for *new* logins (existing cookie sessions unaffected); deploying to 35 needs working access (see Prerequisites).
## The merged directory
One `dc=zb,dc=local` directory; group families partitioned into **non-overlapping gid ranges** (today
both existing GLAuth files reuse 55015505 — the collision to fix). **Each app maps only its own family
and ignores the rest**, so the families coexist with zero conflict.
**Groups**
| Family | Used by | Groups (gidnumber) |
|---|---|---|
| `SCADA-*` (55xx) | ScadaBridge roles (DB-mapped) | Admins 5501, Designers 5502, Deploy-All 5503, Deploy-SiteA 5504, Viewers 5505 |
| OPC-perm (560x) | OtOpcUa + MxGateway OPC-UA write model | ReadOnly 5601, WriteOperate 5602, WriteTune 5603, WriteConfigure 5604, AlarmAck 5605 |
| `Gw*` (561x) | MxGateway dashboard (config-mapped) | GwAdmin 5610, GwReader 5611 |
| `OtOpcUa-*` (57xx) | OtOpcUa AdminUI (DB-mapped) | Admins 5701, Designers 5702, Viewers 5703 |
`SCADA-*` keeps its canonical 55xx numbers (already deployed). The OPC/`Gw` groups move off the old
55015505/5510 into 56xx to clear the clash.
**Users** (all password `password`; uid ranges 50xx ScadaBridge / 51xx MxGateway / 52xx OtOpcUa)
- **`serviceaccount`** (5999, `cn=serviceaccount,dc=zb,dc=local`, `search *` capability) — the *single*
bind account every app uses. Password `serviceaccount123`. ScadaBridge moves to it from `cn=admin`/`password`.
- **`multi-role`** (5005) — member of **every** group → all roles in all three apps (canonical cross-app QA login).
- **`admin`** (5001) — `SCADA-Admins` + `GwAdmin` + `OtOpcUa-Admins` → Administrator everywhere.
- Per-role testers: `designer`/`deployer`/`site-deployer` (ScadaBridge); `gwreader` (MxGateway Viewer);
`otdesigner`/`otviewer` (OtOpcUa); `readonly`/`writeop`/`writetune`/`writeconfig`/`alarmack` (OPC perms).
## Per-app config changes
Each consumer changes only its LDAP `Server` (+ a few keys). Shared service account
`cn=serviceaccount,dc=zb,dc=local` / `serviceaccount123`.
- **ScadaBridge** (`docker/` + `docker-env2/`, central-node-a & -b `appsettings.Central.json`):
`Ldap:Server` `scadabridge-ldap``10.100.0.35`; `ServiceAccountDn` `cn=admin``cn=serviceaccount`,
`ServiceAccountPassword``serviceaccount123`. Rest unchanged (`SCADA-*` DB mappings already seeded).
Retire the `ldap` service in `infra/docker-compose.yml`; sequenced-recreate central nodes.
- **OtOpcUa docker-dev** (`docker-dev/docker-compose.yml`, all host containers) — **the un-stub**:
drop `Security__Ldap__DevStubMode=true`; add `Server=10.100.0.35`, `Port=3893`, `Transport=None`,
`AllowInsecure=true`, `SearchBase=dc=zb,dc=local`, `ServiceAccountDn=cn=serviceaccount,…`,
`ServiceAccountPassword=serviceaccount123`. Seed OtOpcUa DB mappings
`OtOpcUa-Admins→Administrator`, `OtOpcUa-Designers→Designer`, `OtOpcUa-Viewers→Viewer` (system-wide).
- **MxGateway** (windev `C:\publish\mxaccessgw\Server\appsettings.json`): `Ldap:Server`
`localhost``10.100.0.35`; `SearchBase` `dc=lmxopcua``dc=zb,dc=local`; `ServiceAccountDn``…dc=zb,dc=local`.
`Transport=None`/`AllowInsecure=true` already migrated; `GroupToRole` (`GwAdmin`/`GwReader`) unchanged.
Restart `MxAccessGw` (+ dependent `OtOpcUa` svc).
- **OtOpcUa (windev service)**: locate its deployed overlay; repoint `Server``10.100.0.35`,
`SearchBase``dc=zb,dc=local`, service account, and switch dev transport `Ldaps``None`+`AllowInsecure`.
- **Then** stop/disable the windev-local `glauth` service.
## Rollout & rollback
Incremental; **the old glauths stay up until the very end**, so every step is reversible by pointing
`Server` back.
1. Stand up the shared glauth on 35 → verify via `ldapsearch` (bind `serviceaccount`; `multi-role`
`memberOf` spans all families). Nothing repointed yet.
2. Prove reachability from an OrbStack container to `10.100.0.35:3893` (the linchpin) before any app edit.
3. ScadaBridge `:9000` → recreate → browser-verify `multi-role` = 4 roles. Then `:9100`.
4. OtOpcUa docker-dev → un-stub + repoint + seed → recreate → verify.
5. windev MxGateway (backup appsettings) → restart → verify. Then windev OtOpcUa overlay.
6. Only once all green: stop/disable `scadabridge-ldap` + the windev-local glauth.
**Rollback** per consumer: revert the one-line `Server` change (git revert on the Mac; `.bak` restore on
windev) and recreate/restart. Remove the shared glauth = `docker compose down` on 35.
## Testing & verification
- **LDAP layer:** `ldapsearch` bind `serviceaccount`; confirm each test user + `multi-role`'s `memberOf`
across all four families; bind each user to confirm `password`.
- **Per-app browser (macbook Chrome):** ScadaBridge `:9000`/`:9100` `multi-role` → 4 roles (via
`/auth/token`); OtOpcUa `:9200` → seeded roles; MxGateway `10.100.0.48:5130` → Administrator; windev OtOpcUa → AdminUI.
- **Role-gating spot-checks:** `gwreader`→MxGateway Viewer-only; `designer`→ScadaBridge design-only;
`otviewer`→OtOpcUa read-only.
- **Negative:** wrong password rejected everywhere; a user in no family of an app → denied there.
## Prerequisites & open items (resolve in the plan)
1. **Access to `10.100.0.35`** — SSH from this Mac is currently refused (`Permission denied`/connection
reset) and the windev→35 jump is administratively prohibited. Either re-authorize this Mac's key on 35,
or the user runs the final `docker compose up -d`. Artifacts are portable either way.
2. **OtOpcUa group key shape** — confirm OtOpcUa maps on the **short RDN** (`OtOpcUa-Admins`) the shared
lib returns vs the full-DN its `LdapGroupRoleMapping` entity comment shows, before seeding.
3. **OrbStack→LAN reachability** — verify ScadaBridge/OtOpcUa containers can reach `10.100.0.35:3893`
early (likely fine; it's the linchpin). `log()` if any consumer can't reach 35 rather than silently failing.
4. **windev OtOpcUa config path** — discovery step (less is known about this deployment than MxGateway).
## Notes
- `scadaproj` is a plain-files umbrella that is *also* a local git repo; `infra/glauth/` lives here as the
canonical source. Per-app config edits land on a `feat/*` branch per repo (merge on the user's go).
windev edits are deployment-only with `.bak` backups (like the GroupToRole / LDAP-key migrations done
2026-06-04); repo templates optionally aligned.
- Related memory: `multi-role-cross-app-test-user`, `mxgateway-windev-deploy`,
`scadabridge-local-deploy-gotchas`, `auth-audit-normalization-in-progress`.
@@ -0,0 +1,610 @@
# Shared GLAuth Standardization — Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.
**Goal:** Consolidate OtOpcUa, MxAccessGateway, and ScadaBridge **dev/test** auth onto one shared GLAuth directory at `10.100.0.35:3893` (`dc=zb,dc=local`, plaintext), replacing the three separate LDAP setups.
**Architecture:** A single app-neutral GLAuth `config` directory lives in `scadaproj/infra/glauth/` (source of truth) and runs as one container on the shared Docker host `10.100.0.35`. Group families are partitioned into non-overlapping gid ranges (`SCADA-*` 55xx, OPC-perm/`Gw*` 56xx, `OtOpcUa-*` 57xx); each app maps only its own family. Every dev consumer just repoints its LDAP `Server` at `10.100.0.35`. Rollout is incremental and keeps the old glauths running until each consumer is verified.
**Tech Stack:** GLAuth (`glauth/glauth:latest`, TOML `config` datastore), Docker Compose / OrbStack (Mac) + Docker on `10.100.0.35`, .NET 10 apps using the shared `ZB.MOM.WW.Auth.Ldap` (search-then-bind), MSSQL config DBs, Windows/NSSM services on windev (`10.100.0.48`), `ldapsearch` + Chrome (macbook) for verification.
**Design:** [`2026-06-04-shared-glauth-standardization-design.md`](2026-06-04-shared-glauth-standardization-design.md)
**Reference values**
- `password` → sha256 `5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8`
- `serviceaccount123` → sha256 `af29d0e5c9801ae98a999ed3915e1cf428a64b4b62b3cf221b6336cce0398419`
- Shared service account: `cn=serviceaccount,dc=zb,dc=local` / `serviceaccount123`
- All consumer LDAP keys: `Server=10.100.0.35 Port=3893 Transport=None AllowInsecure=true SearchBase=dc=zb,dc=local`
**Branching:** scadaproj artifacts on the current `docs/shared-glauth-standardization` branch. Per-app config edits on a `feat/shared-glauth` branch in each app repo (ScadaBridge, OtOpcUa). windev edits are deployment-only (`.bak` backups), repo templates optionally aligned. Merge on the user's go.
**Operational caveats (read first):**
- **`10.100.0.35` access is currently blocked from this Mac** (SSH refused; windev→35 jump prohibited). **Task 4 is a hard gate** — it needs either this Mac's key re-authorized on 35 *or* the user to run the `docker compose up`. The artifact is portable.
- Tasks that recreate running clusters (ScadaBridge, OtOpcUa) and touch the **live windev host** are operational; their "tests" are `ldapsearch`/`curl`/browser checks with exact expected output. Sequence cluster recreates seed-first to avoid Akka split-brain.
---
## Phase 0 — Author + deploy the shared GLAuth
### Task 0: Write the merged GLAuth `config.toml`
**Classification:** standard
**Estimated implement time:** ~4 min
**Parallelizable with:** Task 1, Task 2
**Files:**
- Create: `/Users/dohertj2/Desktop/scadaproj/infra/glauth/config.toml`
**Step 1: Write the file** with this exact content (merged `dc=zb,dc=local` directory; gid families partitioned; `multi-role` is in every group):
```toml
[ldap]
enabled = true
listen = "0.0.0.0:3893"
[ldaps]
enabled = false
[backend]
datastore = "config"
baseDN = "dc=zb,dc=local"
[behaviors]
# Dev: do not lock out on failed binds (avoids surprises during testing).
LimitFailedBinds = false
# ── Groups ───────────────────────────────────────────────────────────
# ScadaBridge role groups (55xx) — DB-mapped (LdapGroupMappings)
[[groups]]
name = "SCADA-Admins"
gidnumber = 5501
[[groups]]
name = "SCADA-Designers"
gidnumber = 5502
[[groups]]
name = "SCADA-Deploy-All"
gidnumber = 5503
[[groups]]
name = "SCADA-Deploy-SiteA"
gidnumber = 5504
[[groups]]
name = "SCADA-Viewers"
gidnumber = 5505
# OPC-UA permission groups (560x) — OtOpcUa + MxGateway OPC write model
[[groups]]
name = "ReadOnly"
gidnumber = 5601
[[groups]]
name = "WriteOperate"
gidnumber = 5602
[[groups]]
name = "WriteTune"
gidnumber = 5603
[[groups]]
name = "WriteConfigure"
gidnumber = 5604
[[groups]]
name = "AlarmAck"
gidnumber = 5605
# MxGateway dashboard groups (561x) — config-mapped (GroupToRole)
[[groups]]
name = "GwAdmin"
gidnumber = 5610
[[groups]]
name = "GwReader"
gidnumber = 5611
# OtOpcUa AdminUI role groups (57xx) — DB-mapped (LdapGroupRoleMapping)
[[groups]]
name = "OtOpcUa-Admins"
gidnumber = 5701
[[groups]]
name = "OtOpcUa-Designers"
gidnumber = 5702
[[groups]]
name = "OtOpcUa-Viewers"
gidnumber = 5703
# ── Users ────────────────────────────────────────────────────────────
# All passwords are "password" except serviceaccount ("serviceaccount123").
# sha256("password") = 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
# sha256("serviceaccount123") = af29d0e5c9801ae98a999ed3915e1cf428a64b4b62b3cf221b6336cce0398419
# The single bind account every app uses (search-then-bind).
[[users]]
name = "serviceaccount"
uidnumber = 5999
primarygroup = 5601
passsha256 = "af29d0e5c9801ae98a999ed3915e1cf428a64b4b62b3cf221b6336cce0398419"
[[users.capabilities]]
action = "search"
object = "*"
# Cross-app: member of EVERY group → all roles in all three apps.
[[users]]
name = "multi-role"
givenname = "Multi"
sn = "Role"
mail = "multi-role@zb.local"
uidnumber = 5005
primarygroup = 5501
othergroups = [5502, 5503, 5504, 5505, 5601, 5602, 5603, 5604, 5605, 5610, 5611, 5701, 5702, 5703]
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
# Administrator everywhere (admin-equivalent of each app).
[[users]]
name = "admin"
uidnumber = 5001
primarygroup = 5501
othergroups = [5610, 5701]
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
# ScadaBridge single-role testers
[[users]]
name = "designer"
uidnumber = 5002
primarygroup = 5502
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
[[users]]
name = "deployer"
uidnumber = 5003
primarygroup = 5503
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
[[users]]
name = "site-deployer"
uidnumber = 5004
primarygroup = 5504
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
# MxGateway dashboard Viewer tester
[[users]]
name = "gwreader"
uidnumber = 5106
primarygroup = 5611
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
# OPC-UA permission testers
[[users]]
name = "readonly"
uidnumber = 5101
primarygroup = 5601
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
[[users]]
name = "writeop"
uidnumber = 5102
primarygroup = 5602
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
[[users]]
name = "writetune"
uidnumber = 5103
primarygroup = 5603
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
[[users]]
name = "writeconfig"
uidnumber = 5104
primarygroup = 5604
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
[[users]]
name = "alarmack"
uidnumber = 5105
primarygroup = 5605
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
# OtOpcUa single-role testers (admin covers OtOpcUa-Admins)
[[users]]
name = "otdesigner"
uidnumber = 5202
primarygroup = 5702
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
[[users]]
name = "otviewer"
uidnumber = 5203
primarygroup = 5703
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
```
**Step 2: Verify TOML parses** (sanity, no network):
Run: `python3 -c "import tomllib,sys; tomllib.load(open('/Users/dohertj2/Desktop/scadaproj/infra/glauth/config.toml','rb')); print('OK')"`
Expected: `OK`
---
### Task 1: Write the GLAuth `docker-compose.yml`
**Classification:** small
**Estimated implement time:** ~2 min
**Parallelizable with:** Task 0, Task 2
**Files:**
- Create: `/Users/dohertj2/Desktop/scadaproj/infra/glauth/docker-compose.yml`
**Step 1: Write** (single service, bind-mount the config read-only, publish 3893 on all interfaces so cross-host clients reach it):
```yaml
# Shared dev GLAuth for OtOpcUa + MxAccessGateway + ScadaBridge.
# Deploy on the shared Docker host 10.100.0.35: docker compose up -d
# Verify: ldapsearch -x -H ldap://10.100.0.35:3893 \
# -D cn=serviceaccount,dc=zb,dc=local -w serviceaccount123 \
# -b dc=zb,dc=local "(cn=multi-role)" memberOf
name: zb-shared-glauth
services:
glauth:
image: glauth/glauth:latest
container_name: zb-shared-glauth
restart: unless-stopped
ports:
- "3893:3893"
volumes:
- ./config.toml:/app/config/config.cfg:ro
```
---
### Task 2: Write the `README.md` (deploy + verify runbook)
**Classification:** small
**Estimated implement time:** ~3 min
**Parallelizable with:** Task 0, Task 1
**Files:**
- Create: `/Users/dohertj2/Desktop/scadaproj/infra/glauth/README.md`
**Step 1: Write** a runbook covering: purpose (shared dev directory for all 3 apps); the merged directory's group families + gid ranges + the canonical users (`multi-role`/`admin`/`serviceaccount` + per-role testers); **deploy on `10.100.0.35`** (`scp -r infra/glauth dohertj2@10.100.0.35:~/zb-glauth && ssh dohertj2@10.100.0.35 'cd ~/zb-glauth && docker compose up -d'`) with the note that this Mac's SSH access to 35 must be working (else the user runs it); and the **verification** `ldapsearch` commands (bind `serviceaccount`, confirm `multi-role`'s `memberOf` spans all four families; bind each tester). Include the "to add a user/group, edit `config.toml` and `docker compose up -d --force-recreate` (the single-file bind-mount needs a recreate, not a restart)" gotcha.
---
### Task 3: Commit Phase 0 artifacts
**Classification:** trivial
**Estimated implement time:** ~1 min
**Parallelizable with:** none
**Files:** (commit only) — `/Users/dohertj2/Desktop/scadaproj/infra/glauth/*`
**Step 1:** From `/Users/dohertj2/Desktop/scadaproj` (already on `docs/shared-glauth-standardization`):
```bash
git add infra/glauth/config.toml infra/glauth/docker-compose.yml infra/glauth/README.md
git commit -m "feat(glauth): merged shared dev GLAuth directory + compose + runbook (10.100.0.35)"
```
---
### Task 4: Deploy to `10.100.0.35` and verify the directory ⟵ HARD GATE / ACCESS-PREREQUISITE
**Classification:** high-risk
**Estimated implement time:** ~5 min (blocked on 35 access)
**Parallelizable with:** none
**Files:** none (operational)
**Step 1: Resolve access.** Confirm `ssh dohertj2@10.100.0.35 'echo ok'` works. If it does NOT (currently the case from this Mac), STOP and either (a) have the user re-authorize this Mac's key on 35, or (b) hand the user `infra/glauth/` + the deploy command to run on 35. Do not proceed past this gate until GLAuth is up on 35.
**Step 2: Deploy** (once access works). Copy the FILES into the dest dir (not the dir itself) so a
re-deploy doesn't nest them at `~/zb-glauth/glauth/` (the `scp -r dir-into-existing-dir` trap):
```bash
ssh dohertj2@10.100.0.35 'mkdir -p ~/zb-glauth'
scp /Users/dohertj2/Desktop/scadaproj/infra/glauth/config.toml \
/Users/dohertj2/Desktop/scadaproj/infra/glauth/docker-compose.yml \
dohertj2@10.100.0.35:~/zb-glauth/
ssh dohertj2@10.100.0.35 'cd ~/zb-glauth && docker compose up -d --force-recreate && docker ps --filter name=zb-shared-glauth'
```
Expected: `zb-shared-glauth` container `Up`.
**Step 3 (test): Verify the directory** from the Mac via a throwaway ldap client:
```bash
docker run --rm alpine:3.20 sh -c 'apk add --no-progress -q openldap-clients >/dev/null 2>&1 && \
ldapsearch -x -H ldap://10.100.0.35:3893 -D "cn=serviceaccount,dc=zb,dc=local" -w serviceaccount123 \
-b "dc=zb,dc=local" "(cn=multi-role)" memberOf'
```
Expected: `result: 0 Success` and `memberOf` listing all four families — `SCADA-*`, `ReadOnly/Write*/AlarmAck`, `GwAdmin/GwReader`, `OtOpcUa-*`.
**Step 4 (test): Confirm a user binds with `password`:**
```bash
docker run --rm alpine:3.20 sh -c 'apk add --no-progress -q openldap-clients >/dev/null 2>&1 && \
ldapsearch -x -H ldap://10.100.0.35:3893 -D "cn=multi-role,dc=zb,dc=local" -w password \
-b "dc=zb,dc=local" "(cn=multi-role)" cn 2>&1 | grep -i "result:"'
```
Expected: `result: 50 Insufficient access` (bind OK — search denied because multi-role lacks the search capability; a *bad* password would give `result: 49`).
---
## Phase 1 — ScadaBridge repoint (Mac docker)
### Task 5: Repoint the 4 ScadaBridge central-node configs
**Classification:** standard
**Estimated implement time:** ~3 min
**Parallelizable with:** Task 10, Task 11, Task 14, Task 15 (different repos/hosts)
**Files (4 identical edits):**
- Modify: `/Users/dohertj2/Desktop/ScadaBridge/docker/central-node-a/appsettings.Central.json` (`Ldap` block ~lines 2532)
- Modify: `/Users/dohertj2/Desktop/ScadaBridge/docker/central-node-b/appsettings.Central.json`
- Modify: `/Users/dohertj2/Desktop/ScadaBridge/docker-env2/central-node-a/appsettings.Central.json`
- Modify: `/Users/dohertj2/Desktop/ScadaBridge/docker-env2/central-node-b/appsettings.Central.json`
**Step 1:** In each file's `Ldap` block, change three keys (leave `Port`, `Transport`, `AllowInsecure`, `SearchBase` as-is — already `3893`/`None`/`true`/`dc=zb,dc=local`):
- `"Server": "scadabridge-ldap"``"Server": "10.100.0.35"`
- `"ServiceAccountDn": "cn=admin,dc=zb,dc=local"``"ServiceAccountDn": "cn=serviceaccount,dc=zb,dc=local"`
- `"ServiceAccountPassword": "password"``"ServiceAccountPassword": "serviceaccount123"`
**Step 2 (test): Confirm all four files updated:**
Run: `grep -l '"Server": "10.100.0.35"' /Users/dohertj2/Desktop/ScadaBridge/docker*/central-node-*/appsettings.Central.json | wc -l`
Expected: `4`
---
### Task 6: Retire the `scadabridge-ldap` service + prove OrbStack→35 reachability
**Classification:** small
**Estimated implement time:** ~3 min
**Parallelizable with:** Task 5
**Files:**
- Modify: `/Users/dohertj2/Desktop/ScadaBridge/infra/docker-compose.yml` (the `ldap:` service, lines ~4450)
**Step 1 (test FIRST — the linchpin): Verify a container on `scadabridge-net` can reach `10.100.0.35:3893`** before retiring anything:
```bash
docker run --rm --network scadabridge-net alpine:3.20 sh -c \
'apk add --no-progress -q openldap-clients >/dev/null 2>&1 && \
ldapsearch -x -H ldap://10.100.0.35:3893 -D "cn=serviceaccount,dc=zb,dc=local" -w serviceaccount123 -b "dc=zb,dc=local" "(cn=admin)" cn 2>&1 | grep -i "result:"'
```
Expected: `result: 0 Success`. **If unreachable, STOP** — fix networking (OrbStack→LAN) before repointing; do not retire the local glauth.
**Step 2:** Comment out (do not delete — keep for rollback) the `ldap:` service block in `infra/docker-compose.yml`. Stop the old container: `docker stop scadabridge-ldap`. (Leave it stopped, not removed, until Phase 4.)
---
### Task 7: Recreate the `:9000` cluster central nodes + browser-verify
**Classification:** high-risk
**Estimated implement time:** ~5 min (operational)
**Parallelizable with:** none
**Files:** none (operational)
**Step 1:** Recreate the two central nodes to pick up the new config (seed-first to avoid split-brain — recreate `central-a`, wait healthy, then `central-b`):
```bash
cd /Users/dohertj2/Desktop/ScadaBridge/docker && docker compose up -d --force-recreate --no-deps central-node-a
# wait until central-a is serving, then:
docker compose up -d --force-recreate --no-deps central-node-b
```
**Step 2 (test): Token endpoint shows all four roles** (re-runs the full LDAP auth against 35):
```bash
curl -s -m10 -X POST http://localhost:9000/auth/token --data-urlencode username=multi-role --data-urlencode password=password
```
Expected JSON contains `"roles":["Administrator","Designer","Deployer","Viewer"]`.
**Step 3 (test): Browser** (Chrome macbook) — sign out, log in `multi-role`/`password` at `http://localhost:9000/login`; expect the dashboard with ADMIN + DESIGN + DEPLOYMENT nav sections.
---
### Task 8: Recreate the `:9100` cluster central nodes + verify
**Classification:** high-risk
**Estimated implement time:** ~5 min (operational)
**Parallelizable with:** none
**Files:** none (operational)
**Step 1:** As Task 7 but in `/Users/dohertj2/Desktop/ScadaBridge/docker-env2` (recreate `central-node-a` then `-b`).
**Step 2 (test):** `curl -s -m10 -X POST http://localhost:9100/auth/token --data-urlencode username=multi-role --data-urlencode password=password``"roles":["Administrator","Designer","Deployer","Viewer"]`.
---
### Task 9: Commit ScadaBridge edits on a branch
**Classification:** trivial
**Estimated implement time:** ~1 min
**Parallelizable with:** none
**Files:** (commit) the 4 central-node json + `infra/docker-compose.yml`
**Step 1:**
```bash
cd /Users/dohertj2/Desktop/ScadaBridge && git checkout -b feat/shared-glauth
git add docker/central-node-*/appsettings.Central.json docker-env2/central-node-*/appsettings.Central.json infra/docker-compose.yml
git commit -m "feat(auth): point dev clusters at shared GLAuth 10.100.0.35; retire local scadabridge-ldap"
```
(Do not merge/push — wait for the user's go.)
---
## Phase 2 — OtOpcUa docker-dev un-stub (Mac docker)
### Task 10: Confirm group-key shape, then add `LdapGroupRoleMapping` seed rows
**Classification:** standard
**Estimated implement time:** ~4 min
**Parallelizable with:** Task 5, Task 14, Task 15
**Files:**
- Modify: `/Users/dohertj2/Desktop/OtOpcUa/docker-dev/seed/seed-clusters.sql`
- Read (gate): `/Users/dohertj2/Desktop/scadaproj/ZB.MOM.WW.Auth/src/ZB.MOM.WW.Auth.Ldap/LdapAuthService.cs`
**Step 1 (gate): Confirm the runtime group string is the bare RDN** (`OtOpcUa-Admins`), not a full DN. Read `LdapAuthService.cs` and find where it builds the returned `Groups` from `memberOf`; confirm it strips each DN to its first RDN *value*. Cross-check: ScadaBridge's DB mappings use bare `SCADA-Admins` and work today against the same glauth `groupformat=ou` (so memberOf is `ou=SCADA-Admins,...` → returned as `SCADA-Admins`). Conclusion to lock: seed `LdapGroup = 'OtOpcUa-Admins'` (bare). If the code instead returns full DNs, STOP and seed the full DN form — but the evidence says bare.
**Step 2:** Append idempotent INSERTs to `seed-clusters.sql` (table `dbo.LdapGroupRoleMapping`; `Role` stored as the enum NAME string; system-wide rows ⇒ `ClusterId = NULL`, `IsSystemWide = 1`):
```sql
-- Shared-GLAuth dev: OtOpcUa AdminUI role mappings (system-wide).
-- Group keys are the BARE RDN names the shared ZB.MOM.WW.Auth.Ldap returns.
IF NOT EXISTS (SELECT 1 FROM dbo.LdapGroupRoleMapping WHERE LdapGroup = 'OtOpcUa-Admins' AND ClusterId IS NULL)
INSERT INTO dbo.LdapGroupRoleMapping (Id, LdapGroup, Role, ClusterId, IsSystemWide, CreatedAtUtc, Notes)
VALUES (NEWID(), 'OtOpcUa-Admins', 'Administrator', NULL, 1, SYSUTCDATETIME(), 'shared-glauth dev seed');
IF NOT EXISTS (SELECT 1 FROM dbo.LdapGroupRoleMapping WHERE LdapGroup = 'OtOpcUa-Designers' AND ClusterId IS NULL)
INSERT INTO dbo.LdapGroupRoleMapping (Id, LdapGroup, Role, ClusterId, IsSystemWide, CreatedAtUtc, Notes)
VALUES (NEWID(), 'OtOpcUa-Designers', 'Designer', NULL, 1, SYSUTCDATETIME(), 'shared-glauth dev seed');
IF NOT EXISTS (SELECT 1 FROM dbo.LdapGroupRoleMapping WHERE LdapGroup = 'OtOpcUa-Viewers' AND ClusterId IS NULL)
INSERT INTO dbo.LdapGroupRoleMapping (Id, LdapGroup, Role, ClusterId, IsSystemWide, CreatedAtUtc, Notes)
VALUES (NEWID(), 'OtOpcUa-Viewers', 'Viewer', NULL, 1, SYSUTCDATETIME(), 'shared-glauth dev seed');
```
---
### Task 11: Un-stub the OtOpcUa docker-dev host containers
**Classification:** standard
**Estimated implement time:** ~5 min
**Parallelizable with:** Task 5, Task 14, Task 15
**Files:**
- Modify: `/Users/dohertj2/Desktop/OtOpcUa/docker-dev/docker-compose.yml` (the 6 admin/site containers: `admin-a` ~L100, `admin-b` ~L117, `site-a-1` ~L170, `site-a-2` ~L193, `site-b-1` ~L215, `site-b-2` ~L238)
**Step 1:** In each of the 6 containers' `environment:`, replace the single `Security__Ldap__DevStubMode: "true"` line with the real-LDAP block:
```yaml
Security__Ldap__Enabled: "true"
Security__Ldap__DevStubMode: "false"
Security__Ldap__Server: "10.100.0.35"
Security__Ldap__Port: "3893"
Security__Ldap__Transport: "None"
Security__Ldap__AllowInsecure: "true"
Security__Ldap__SearchBase: "dc=zb,dc=local"
Security__Ldap__ServiceAccountDn: "cn=serviceaccount,dc=zb,dc=local"
Security__Ldap__ServiceAccountPassword: "serviceaccount123"
```
(Driver-only `driver-a`/`driver-b` have no LDAP block — leave them.)
**Step 2 (test): Confirm 6 containers updated, 0 DevStub left:**
Run: `grep -c 'Security__Ldap__Server: "10.100.0.35"' /Users/dohertj2/Desktop/OtOpcUa/docker-dev/docker-compose.yml``6`; and `grep -c 'DevStubMode: "true"' …/docker-compose.yml``0`.
---
### Task 12: Apply seed + recreate `otopcua-dev` + browser-verify
**Classification:** high-risk
**Estimated implement time:** ~5 min (operational)
**Parallelizable with:** none
**Files:** none (operational)
**Step 1: Apply the new mapping rows to the running config DB** (host port 14330):
```bash
docker exec otopcua-dev-sql-1 /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'OtOpcUa!Dev123' -No -d OtOpcUa -Q "$(sed -n '/OtOpcUa-Admins/,/shared-glauth dev seed.);/p' /Users/dohertj2/Desktop/OtOpcUa/docker-dev/seed/seed-clusters.sql)"
```
(or simpler: re-run the seed container `docker compose -f docker-dev/docker-compose.yml up cluster-seed`). Verify: `… -Q "SELECT LdapGroup,Role FROM dbo.LdapGroupRoleMapping WHERE IsSystemWide=1"` → the 3 OtOpcUa-* rows.
**Step 2: Recreate the 6 admin/site host containers** (seed-first per cluster — recreate `admin-a` then `admin-b`; `site-a-1` then `site-a-2`; `site-b-1` then `site-b-2`):
```bash
cd /Users/dohertj2/Desktop/OtOpcUa/docker-dev
for n in admin-a admin-b site-a-1 site-a-2 site-b-1 site-b-2; do docker compose up -d --force-recreate --no-deps $n; sleep 3; done
```
**Step 3 (test): Browser** — log in `multi-role`/`password` at `http://localhost:9200/login`; expect the AdminUI Overview, SESSION panel showing `multi-role` + **Administrator** (from `OtOpcUa-Admins`→Administrator). Confirms the un-stub + real bind + DB mapping all work.
---
### Task 13: Commit OtOpcUa edits on a branch
**Classification:** trivial
**Estimated implement time:** ~1 min
**Parallelizable with:** none
**Files:** (commit) `docker-dev/docker-compose.yml`, `docker-dev/seed/seed-clusters.sql`
**Step 1:**
```bash
cd /Users/dohertj2/Desktop/OtOpcUa && git checkout -b feat/shared-glauth
git add docker-dev/docker-compose.yml docker-dev/seed/seed-clusters.sql
git commit -m "feat(auth): un-stub docker-dev onto shared GLAuth 10.100.0.35 + seed OtOpcUa-* role mappings"
```
(Do not merge/push.)
---
## Phase 3 — windev repoint + retire windev-local glauth (live host)
### Task 14: Repoint MxGateway (windev) at the shared GLAuth
**Classification:** high-risk
**Estimated implement time:** ~5 min (operational, live host)
**Parallelizable with:** Task 5, Task 10, Task 11
**Files:** (windev, deployment-only) `C:\publish\mxaccessgw\Server\appsettings.json` (`MxGateway:Ldap`)
**Step 1: Back up** `appsettings.json``appsettings.json.bak-20260604-glauth35` (skip if exists).
**Step 2: Edit `MxGateway:Ldap`** (literal replacements; preserve the rest, incl. the `Transport=None`/`AllowInsecure=true` migrated 2026-06-04, and `GroupToRole`):
- `"Server": "localhost"``"Server": "10.100.0.35"`
- `"SearchBase": "dc=lmxopcua,dc=local"``"SearchBase": "dc=zb,dc=local"`
- `"ServiceAccountDn": "cn=serviceaccount,dc=lmxopcua,dc=local"``"ServiceAccountDn": "cn=serviceaccount,dc=zb,dc=local"`
(`ServiceAccountPassword` stays `serviceaccount123`.) Use a `-File` PowerShell script (`[IO.File]::WriteAllText` after `.Replace(...)`), validate JSON parses.
**Step 3:** `Restart-Service MxAccessGw -Force; Start-Service OtOpcUa` (cascades to the dependent OtOpcUa svc — start it back).
**Step 4 (test):** From the Mac, `POST http://10.100.0.48:5130/auth/login` (GET `/login` for the antiforgery token+cookie first) with `username=multi-role&password=password``302 Location: /` (success). Browser-verify the dashboard logs in as `multi-role` (Administrator).
---
### Task 15: Repoint OtOpcUa (windev service) + switch transport to plaintext
**Classification:** high-risk
**Estimated implement time:** ~5 min (operational, live host)
**Parallelizable with:** Task 5, Task 10, Task 11
**Files:** (windev, deployment-only) `C:\publish\lmxopcua\appsettings.json` (`Security:Ldap`) — **discover any per-role overlay first** (`appsettings.admin.json`/`appsettings.driver.json` in `C:\publish\lmxopcua\` or `C:\publish\lmxopcua-admin\`; the live binary is `C:\publish\lmxopcua\OtOpcUa.Server.exe`).
**Step 1: Discovery**`Get-ChildItem C:\publish\lmxopcua\appsettings*.json` and inspect which file holds the live `Security:Ldap` (base + any `appsettings.admin.json` overlay that sets `Transport=Ldaps`). Back up whatever you edit.
**Step 2: Edit `Security:Ldap`** in the live config (and the admin overlay if present):
- `Server``10.100.0.35`; `SearchBase``dc=zb,dc=local`; `Transport` `Ldaps``None`; add/set `AllowInsecure` `true`; `ServiceAccountDn``cn=serviceaccount,dc=zb,dc=local`, `ServiceAccountPassword``serviceaccount123`; ensure `DevStubMode=false`.
**Step 3:** `Restart-Service OtOpcUa` (note the dependency direction: `MxAccessGw` depends on `OtOpcUa` — restarting OtOpcUa may require `-Force` and a follow-up `Start-Service MxAccessGw`; verify both Running).
**Step 4 (test):** Browser-verify the windev OtOpcUa AdminUI logs in as `multi-role` → Administrator. (Locate its dashboard URL during discovery.)
---
### Task 16: Stop/disable the windev-local glauth
**Classification:** small
**Estimated implement time:** ~2 min (operational)
**Parallelizable with:** none
**Files:** none (windev service)
**Step 1 (only after Tasks 14 + 15 verify green):** `Stop-Service glauth; Set-Service glauth -StartupType Manual` (disable autostart but keep installed for rollback). Keep `C:\publish\glauth\glauth.cfg` + the `glauth.cfg.bak-multirole-20260604` backup in place.
**Step 2 (test):** Re-run Task 14/15 logins once more to confirm windev auth still works with the local glauth down (proves they're truly on 35).
---
## Phase 4 — Final verification + housekeeping
### Task 17: Full cross-app verification matrix
**Classification:** high-risk
**Estimated implement time:** ~5 min (operational)
**Parallelizable with:** none
**Files:** none (operational)
**Step 1 (positive):** `multi-role`/`password` logs in on all five surfaces — ScadaBridge `:9000` + `:9100` (4 roles via `/auth/token`), OtOpcUa `:9200` (Administrator), MxGateway `10.100.0.48:5130` (Administrator), windev OtOpcUa.
**Step 2 (role-gating):** `gwreader`/`password` → MxGateway dashboard **Viewer-only** (no API-Keys/Settings admin pages); `designer`/`password` → ScadaBridge design nav but not ADMIN; `otviewer`/`password` → OtOpcUa read-only.
**Step 3 (negative):** wrong password rejected on every surface; a `SCADA-*`-only user (`designer`) gets **denied** on the MxGateway dashboard (no `Gw*` group). Record each result.
---
### Task 18: Update memory, design status, and finalize branches
**Classification:** small
**Estimated implement time:** ~4 min
**Parallelizable with:** none
**Files:**
- Update memory: `multi-role-cross-app-test-user.md` (now backed by the shared 35 GLAuth), `mxgateway-windev-deploy.md` + `scadabridge-local-deploy-gotchas.md` (repointed to 35), add a new `shared-glauth-on-35.md` (the directory layout, gid families, deploy/verify runbook, access caveat) + `MEMORY.md` index lines.
- Update: design doc status → "implemented".
- (Optional) align repo template appsettings (MxGateway/ScadaBridge) on the `feat/shared-glauth` branches so a clean redeploy doesn't reintroduce old keys.
**Step 1:** Write the memory updates. **Step 2:** Mark the design doc implemented. **Step 3:** Summarize branch state (scadaproj `docs/shared-glauth-standardization`; app `feat/shared-glauth` branches committed, not merged) and ask the user about merging.
---
## Execution notes
- **Phases 1, 2, 3 are independent** after Task 4 (different repos/hosts) — their first tasks (5, 10/11, 14/15) are mutually `Parallelizable`. Within a phase, recreate/verify tasks are sequential.
- Old glauths stay up until Tasks 6/16; every repoint is reversible by reverting the one-line `Server` change and recreating/restarting.
- Several tasks are **operational** (recreate clusters, live windev, the 35 deploy) — not code-with-unit-tests; their "tests" are the exact `ldapsearch`/`curl`/browser checks given.
@@ -0,0 +1,25 @@
{
"planPath": "docs/plans/2026-06-04-shared-glauth-standardization.md",
"tasks": [
{"id": 0, "subject": "Task 0: Write merged GLAuth config.toml", "status": "completed"},
{"id": 1, "subject": "Task 1: Write GLAuth docker-compose.yml", "status": "completed"},
{"id": 2, "subject": "Task 2: Write GLAuth README runbook", "status": "completed"},
{"id": 3, "subject": "Task 3: Commit Phase 0 artifacts", "status": "completed", "blockedBy": [0, 1, 2]},
{"id": 4, "subject": "Task 4: Deploy to 10.100.0.35 + verify directory (GATE)", "status": "completed", "blockedBy": [3]},
{"id": 5, "subject": "Task 5: Repoint 4 ScadaBridge central-node configs", "status": "completed", "blockedBy": [4]},
{"id": 6, "subject": "Task 6: Retire scadabridge-ldap + prove OrbStack->35 reachability", "status": "completed", "blockedBy": [4]},
{"id": 7, "subject": "Task 7: Recreate :9000 central nodes + browser-verify", "status": "completed", "blockedBy": [5, 6]},
{"id": 8, "subject": "Task 8: Recreate :9100 central nodes + verify", "status": "completed", "blockedBy": [7]},
{"id": 9, "subject": "Task 9: Commit ScadaBridge edits on feat/shared-glauth", "status": "completed", "blockedBy": [7, 8]},
{"id": 10, "subject": "Task 10: Confirm group-key shape + seed OtOpcUa-* mappings", "status": "completed", "blockedBy": [4]},
{"id": 11, "subject": "Task 11: Un-stub OtOpcUa docker-dev host containers", "status": "completed", "blockedBy": [4]},
{"id": 12, "subject": "Task 12: Apply seed + recreate otopcua-dev + verify", "status": "completed", "blockedBy": [10, 11]},
{"id": 13, "subject": "Task 13: Commit OtOpcUa edits on feat/shared-glauth", "status": "completed", "blockedBy": [12]},
{"id": 14, "subject": "Task 14: Repoint MxGateway (windev) at shared GLAuth", "status": "completed", "blockedBy": [4]},
{"id": 15, "subject": "Task 15: Repoint OtOpcUa (windev) [resolved by discovery: headless OPC server, no LDAP login]", "status": "completed", "blockedBy": [4]},
{"id": 16, "subject": "Task 16: Stop/disable windev-local glauth", "status": "completed", "blockedBy": [14, 15]},
{"id": 17, "subject": "Task 17: Full cross-app verification matrix", "status": "completed", "blockedBy": [7, 8, 12, 14, 15, 16]},
{"id": 18, "subject": "Task 18: Update memory, design status, finalize branches", "status": "completed", "blockedBy": [17, 9, 13]}
],
"lastUpdated": "2026-06-04"
}
+93
View File
@@ -0,0 +1,93 @@
# Shared dev GLAuth (`dc=zb,dc=local`)
One [GLAuth](https://github.com/glauth/glauth) directory that **all three sister apps use for
dev/test auth** — OtOpcUa, MxAccessGateway, ScadaBridge. It runs as a single container on the
shared Docker host **`10.100.0.35:3893`** (plaintext LDAP). This is the app-neutral source of
truth; each app just points its `…Ldap:Server` at `10.100.0.35`.
> Scope: **dev/test only**. Production uses real corporate AD. See
> [`../../docs/plans/2026-06-04-shared-glauth-standardization-design.md`](../../docs/plans/2026-06-04-shared-glauth-standardization-design.md).
## Directory layout
Group families are partitioned into **non-overlapping gid ranges** so the three apps coexist —
each app maps only its own family and ignores the rest.
| Family | Used by | Groups (gidnumber) |
|---|---|---|
| `SCADA-*` (55xx) | ScadaBridge roles (DB-mapped) | Admins 5501, Designers 5502, Deploy-All 5503, Deploy-SiteA 5504, Viewers 5505 |
| OPC-perm (560x) | OtOpcUa + MxGateway OPC-UA write model | ReadOnly 5601, WriteOperate 5602, WriteTune 5603, WriteConfigure 5604, AlarmAck 5605 |
| `Gw*` (561x) | MxGateway dashboard (config-mapped) | GwAdmin 5610, GwReader 5611 |
| `OtOpcUa-*` (57xx) | OtOpcUa AdminUI (DB-mapped) | Admins 5701, Designers 5702, Viewers 5703 |
**Users** (all password `password` except `serviceaccount``serviceaccount123`):
- **`serviceaccount`** (`cn=serviceaccount,dc=zb,dc=local`) — the single bind account every app
uses for search-then-bind. Has a `search *` capability.
- **`multi-role`** — member of **every** group → all roles in all three apps (canonical cross-app login).
- **`admin`** — `SCADA-Admins` + `GwAdmin` + `OtOpcUa-Admins` → Administrator everywhere.
- Per-role testers: `designer` / `deployer` / `site-deployer` (ScadaBridge); `gw-viewer`
(MxGateway Viewer); `otdesigner` / `otviewer` (OtOpcUa); `opc-readonly` / `opc-writeop` /
`opc-writetune` / `opc-writeconfig` / `opc-alarmack` (OPC perms).
> **Naming rule:** a tester username must **not** case-collide with a group name. GLAuth exposes
> each group as `cn=<Group>` under `ou=users`, so a case-insensitive `(cn=x)` search would match
> both the user and the group (two entries → the shared lib's "exactly one entry" rule fails the
> bind). That's why the OPC/Gw testers are `opc-*` / `gw-viewer`, not `readonly` / `gwreader`.
## Deploy on `10.100.0.35`
> **Access note:** deploying needs working SSH/Docker access to `10.100.0.35`. If your key is not
> authorized there, hand this folder to whoever administers the box and have them run the same
> `docker compose up -d`. The artifact is self-contained.
```bash
# From this repo root. Copy the FILES into the dest (not the dir) so a re-deploy
# doesn't nest them at ~/zb-glauth/glauth/ (scp -r of a dir into an existing dir).
ssh dohertj2@10.100.0.35 'mkdir -p ~/zb-glauth'
scp infra/glauth/config.toml infra/glauth/docker-compose.yml dohertj2@10.100.0.35:~/zb-glauth/
ssh dohertj2@10.100.0.35 'cd ~/zb-glauth && docker compose up -d --force-recreate && docker ps --filter name=zb-shared-glauth'
```
## Verify
Bind as the service account and confirm `multi-role` spans all four families:
```bash
ldapsearch -x -H ldap://10.100.0.35:3893 \
-D cn=serviceaccount,dc=zb,dc=local -w serviceaccount123 \
-b dc=zb,dc=local "(cn=multi-role)" memberOf
# → result: 0 Success. memberOf comes back as group DNs (e.g. ou=SCADA-Admins,ou=groups,dc=zb,dc=local)
# spanning all four families: SCADA-*, ReadOnly/Write*/AlarmAck, GwAdmin/GwReader, OtOpcUa-*.
# (The shared ZB.MOM.WW.Auth.Ldap lib strips each to its bare RDN, e.g. "SCADA-Admins", at login.)
```
Confirm a user authenticates with `password` (a bad password returns `result: 49`; this user lacks
the search capability, so a successful bind shows `result: 50 Insufficient access` on the search):
```bash
ldapsearch -x -H ldap://10.100.0.35:3893 \
-D cn=multi-role,dc=zb,dc=local -w password \
-b dc=zb,dc=local "(cn=multi-role)" cn
```
No `ldapsearch` locally? Run it from a throwaway container:
```bash
docker run --rm alpine:3.20 sh -c 'apk add -q openldap-clients >/dev/null 2>&1 && \
ldapsearch -x -H ldap://10.100.0.35:3893 -D cn=serviceaccount,dc=zb,dc=local -w serviceaccount123 \
-b dc=zb,dc=local "(cn=multi-role)" memberOf'
```
## Editing the directory
GLAuth uses the `config` datastore (this `config.toml`, mounted read-only). To add/change a user
or group, edit `config.toml` and **recreate** the container — a plain `restart` keeps the stale
file (single-file Docker bind-mount inode trap):
```bash
docker compose up -d --force-recreate
```
Group `gidnumber`s and user `uidnumber`s must stay **unique** across the whole file; keep new
groups inside the per-app range (55xx / 56xx / 57xx) so the families don't collide.
+169
View File
@@ -0,0 +1,169 @@
[ldap]
enabled = true
listen = "0.0.0.0:3893"
[ldaps]
enabled = false
[backend]
datastore = "config"
baseDN = "dc=zb,dc=local"
[behaviors]
# Dev: do not lock out on failed binds (avoids surprises during testing).
LimitFailedBinds = false
# ── Groups ───────────────────────────────────────────────────────────
# ScadaBridge role groups (55xx) — DB-mapped (LdapGroupMappings)
[[groups]]
name = "SCADA-Admins"
gidnumber = 5501
[[groups]]
name = "SCADA-Designers"
gidnumber = 5502
[[groups]]
name = "SCADA-Deploy-All"
gidnumber = 5503
[[groups]]
name = "SCADA-Deploy-SiteA"
gidnumber = 5504
[[groups]]
name = "SCADA-Viewers"
gidnumber = 5505
# OPC-UA permission groups (560x) — OtOpcUa + MxGateway OPC write model
[[groups]]
name = "ReadOnly"
gidnumber = 5601
[[groups]]
name = "WriteOperate"
gidnumber = 5602
[[groups]]
name = "WriteTune"
gidnumber = 5603
[[groups]]
name = "WriteConfigure"
gidnumber = 5604
[[groups]]
name = "AlarmAck"
gidnumber = 5605
# MxGateway dashboard groups (561x) — config-mapped (GroupToRole)
[[groups]]
name = "GwAdmin"
gidnumber = 5610
[[groups]]
name = "GwReader"
gidnumber = 5611
# OtOpcUa AdminUI role groups (57xx) — DB-mapped (LdapGroupRoleMapping)
[[groups]]
name = "OtOpcUa-Admins"
gidnumber = 5701
[[groups]]
name = "OtOpcUa-Designers"
gidnumber = 5702
[[groups]]
name = "OtOpcUa-Viewers"
gidnumber = 5703
# ── Users ────────────────────────────────────────────────────────────
# All passwords are "password" except serviceaccount ("serviceaccount123").
# sha256("password") = 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
# sha256("serviceaccount123") = af29d0e5c9801ae98a999ed3915e1cf428a64b4b62b3cf221b6336cce0398419
# The single bind account every app uses (search-then-bind).
[[users]]
name = "serviceaccount"
uidnumber = 5999
primarygroup = 5601
passsha256 = "af29d0e5c9801ae98a999ed3915e1cf428a64b4b62b3cf221b6336cce0398419"
[[users.capabilities]]
action = "search"
object = "*"
# Cross-app: member of EVERY group → all roles in all three apps.
[[users]]
name = "multi-role"
givenname = "Multi"
sn = "Role"
mail = "multi-role@zb.local"
uidnumber = 5005
primarygroup = 5501
othergroups = [5502, 5503, 5504, 5505, 5601, 5602, 5603, 5604, 5605, 5610, 5611, 5701, 5702, 5703]
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
# Administrator everywhere (admin-equivalent of each app).
[[users]]
name = "admin"
uidnumber = 5001
primarygroup = 5501
othergroups = [5610, 5701]
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
# ScadaBridge single-role testers
[[users]]
name = "designer"
uidnumber = 5002
primarygroup = 5502
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
[[users]]
name = "deployer"
uidnumber = 5003
primarygroup = 5503
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
[[users]]
name = "site-deployer"
uidnumber = 5004
primarygroup = 5504
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
# MxGateway dashboard Viewer tester.
# NOTE: tester usernames must NOT case-collide with a group name — glauth exposes
# each group as cn=<Group> under ou=users, so a case-insensitive (cn=X) search
# would match both the user and the group (ambiguous → auth fails). Hence gw-viewer
# (not "gwreader" which collides with the GwReader group), opc-* below, etc.
[[users]]
name = "gw-viewer"
uidnumber = 5106
primarygroup = 5611
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
# OPC-UA permission testers
[[users]]
name = "opc-readonly"
uidnumber = 5101
primarygroup = 5601
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
[[users]]
name = "opc-writeop"
uidnumber = 5102
primarygroup = 5602
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
[[users]]
name = "opc-writetune"
uidnumber = 5103
primarygroup = 5603
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
[[users]]
name = "opc-writeconfig"
uidnumber = 5104
primarygroup = 5604
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
[[users]]
name = "opc-alarmack"
uidnumber = 5105
primarygroup = 5605
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
# OtOpcUa single-role testers (admin covers OtOpcUa-Admins)
[[users]]
name = "otdesigner"
uidnumber = 5202
primarygroup = 5702
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
[[users]]
name = "otviewer"
uidnumber = 5203
primarygroup = 5703
passsha256 = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
+15
View File
@@ -0,0 +1,15 @@
# Shared dev GLAuth for OtOpcUa + MxAccessGateway + ScadaBridge.
# Deploy on the shared Docker host 10.100.0.35: docker compose up -d
# Verify: ldapsearch -x -H ldap://10.100.0.35:3893 \
# -D cn=serviceaccount,dc=zb,dc=local -w serviceaccount123 \
# -b dc=zb,dc=local "(cn=multi-role)" memberOf
name: zb-shared-glauth
services:
glauth:
image: glauth/glauth:latest
container_name: zb-shared-glauth
restart: unless-stopped
ports:
- "3893:3893"
volumes:
- ./config.toml:/app/config/config.cfg:ro