Files
scadaproj/docs/plans/2026-06-04-shared-glauth-standardization.md
T
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

28 KiB
Raw Blame History

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

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):

[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):

# 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):

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):

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:

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:

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:

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):

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):

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:

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):

-- 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:

      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.yml6; and grep -c 'DevStubMode: "true"' …/docker-compose.yml0.


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):

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):

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:

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.jsonappsettings.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=password302 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: DiscoveryGet-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):

  • Server10.100.0.35; SearchBasedc=zb,dc=local; Transport LdapsNone; add/set AllowInsecure true; ServiceAccountDncn=serviceaccount,dc=zb,dc=local, ServiceAccountPasswordserviceaccount123; 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.