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).
28 KiB
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→ sha2565e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8serviceaccount123→ sha256af29d0e5c9801ae98a999ed3915e1cf428a64b4b62b3cf221b6336cce0398419- 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.35access 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 thedocker 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(Ldapblock ~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(theldap: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:
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.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):
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.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"(ServiceAccountPasswordstaysserviceaccount123.) Use a-FilePowerShell script ([IO.File]::WriteAllTextafter.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;TransportLdaps→None; add/setAllowInsecuretrue;ServiceAccountDn→cn=serviceaccount,dc=zb,dc=local,ServiceAccountPassword→serviceaccount123; ensureDevStubMode=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 newshared-glauth-on-35.md(the directory layout, gid families, deploy/verify runbook, access caveat) +MEMORY.mdindex lines. - Update: design doc status → "implemented".
- (Optional) align repo template appsettings (MxGateway/ScadaBridge) on the
feat/shared-glauthbranches 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
Serverchange 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.