diff --git a/docs/plans/2026-06-04-shared-glauth-standardization.md b/docs/plans/2026-06-04-shared-glauth-standardization.md new file mode 100644 index 0000000..0d17de1 --- /dev/null +++ b/docs/plans/2026-06-04-shared-glauth-standardization.md @@ -0,0 +1,606 @@ +# 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): +```bash +scp -r /Users/dohertj2/Desktop/scadaproj/infra/glauth dohertj2@10.100.0.35:~/zb-glauth +ssh dohertj2@10.100.0.35 'cd ~/zb-glauth && docker compose up -d && 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 25–32) +- 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 ~44–50) + +**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. diff --git a/docs/plans/2026-06-04-shared-glauth-standardization.md.tasks.json b/docs/plans/2026-06-04-shared-glauth-standardization.md.tasks.json new file mode 100644 index 0000000..068c63a --- /dev/null +++ b/docs/plans/2026-06-04-shared-glauth-standardization.md.tasks.json @@ -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": "pending"}, + {"id": 1, "subject": "Task 1: Write GLAuth docker-compose.yml", "status": "pending"}, + {"id": 2, "subject": "Task 2: Write GLAuth README runbook", "status": "pending"}, + {"id": 3, "subject": "Task 3: Commit Phase 0 artifacts", "status": "pending", "blockedBy": [0, 1, 2]}, + {"id": 4, "subject": "Task 4: Deploy to 10.100.0.35 + verify directory (GATE)", "status": "pending", "blockedBy": [3]}, + {"id": 5, "subject": "Task 5: Repoint 4 ScadaBridge central-node configs", "status": "pending", "blockedBy": [4]}, + {"id": 6, "subject": "Task 6: Retire scadabridge-ldap + prove OrbStack->35 reachability", "status": "pending", "blockedBy": [4]}, + {"id": 7, "subject": "Task 7: Recreate :9000 central nodes + browser-verify", "status": "pending", "blockedBy": [5, 6]}, + {"id": 8, "subject": "Task 8: Recreate :9100 central nodes + verify", "status": "pending", "blockedBy": [7]}, + {"id": 9, "subject": "Task 9: Commit ScadaBridge edits on feat/shared-glauth", "status": "pending", "blockedBy": [7, 8]}, + {"id": 10, "subject": "Task 10: Confirm group-key shape + seed OtOpcUa-* mappings", "status": "pending", "blockedBy": [4]}, + {"id": 11, "subject": "Task 11: Un-stub OtOpcUa docker-dev host containers", "status": "pending", "blockedBy": [4]}, + {"id": 12, "subject": "Task 12: Apply seed + recreate otopcua-dev + verify", "status": "pending", "blockedBy": [10, 11]}, + {"id": 13, "subject": "Task 13: Commit OtOpcUa edits on feat/shared-glauth", "status": "pending", "blockedBy": [12]}, + {"id": 14, "subject": "Task 14: Repoint MxGateway (windev) at shared GLAuth", "status": "pending", "blockedBy": [4]}, + {"id": 15, "subject": "Task 15: Repoint OtOpcUa (windev) + transport Ldaps->None", "status": "pending", "blockedBy": [4]}, + {"id": 16, "subject": "Task 16: Stop/disable windev-local glauth", "status": "pending", "blockedBy": [14, 15]}, + {"id": 17, "subject": "Task 17: Full cross-app verification matrix", "status": "pending", "blockedBy": [7, 8, 12, 14, 15, 16]}, + {"id": 18, "subject": "Task 18: Update memory, design status, finalize branches", "status": "pending", "blockedBy": [17, 9, 13]} + ], + "lastUpdated": "2026-06-04" +}