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.
This commit is contained in:
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user