Files
scadaproj/ldap_mapping.md

287 lines
15 KiB
Markdown

# `UG_WW_*` → app role mapping (proposal)
Maps the Zimmer NAM `UG_WW_*` Active Directory groups onto the roles each ZB.MOM.WW app
actually implements. Inputs: the 39-group inventory in [`ldap_setup.md`](ldap_setup.md)
(enumerated live 2026-08-12) and the standardized
[`CANONICAL-ROLES.md`](components/auth/spec/CANONICAL-ROLES.md).
> **Status: PROPOSAL.** The mechanical parts (`_RO_`, `_OPERATOR`, `APP_IDE`, `APP_ADMIN`)
> are safe to adopt. Four items marked **⚠ DECIDE** need a human answer before anyone
> writes them into a production config — they are listed in
> [§6](#6-open-questions--decide-before-adopting) and none of them can be resolved from the
> directory alone. Ignition's role set is deliberately out of scope.
>
> ⚠️ **ignitionoee (router): ON HOLD** (decision 2026-08-13) — do **not** set up its LDAP.
> Its role vocabulary is outside the canonical set (admin role is literally `"Admin"`),
> so no existing `UG_WW_*` group maps to it; wiring it to NAM AD requires minting **new**
> AD groups first. Revisit only once those groups exist and their mapping is decided.
## 1. The app role vocabularies (verified in source 2026-08-12)
Each app implements only a **subset** of the canonical six. This is the constraint that
shapes everything below — there is no point mapping a group to a role an app does not have.
| App | Roles it actually implements | Where the mapping lives |
|---|---|---|
| **OtOpcUa** (AdminUI) | `Viewer`, `Designer`, `Administrator` | `Security:Ldap:GroupToRole` (appsettings, merged with a DB overlay) |
| **ScadaBridge** | `Viewer`, `Operator`, `Verifier`, `Designer`, `Deployer`, `Administrator` | `LdapGroupMapping` table (database, not config) |
| **MxAccessGateway** | `Viewer`, `Administrator` | `MxGateway:Dashboard:GroupToRole` |
| **HistorianGateway** | `Viewer`, `Administrator` | `Dashboard:GroupToRole` |
Three things follow that are easy to get wrong:
- **OtOpcUa's LDAP mapping governs the AdminUI only.** Since Gitea #520 (2026-08-01) the
OPC UA data plane does not use LDAP at all — a client presents an `otua_` API key as an
OPC UA UserName token. So a plant operator who only reads tags needs **no AD group here**;
granting one gives them the configuration UI, which is not what anyone means by
"operator access". The `CANONICAL-ROLES.md` row showing OtOpcUa data-plane bundles
predates that change and no longer applies to LDAP.
- **`Administrator` is the literal string in all four apps.** MxGateway's constant is named
`DashboardRoles.Admin` but its value is `"Administrator"`, and its validator rejects
anything that is not `Administrator` or `Viewer`.
- **Every app fails closed on zero matched groups.** A user whose groups match no entry is
denied. So an unmapped group grants nothing — the mapping does not have to be exhaustive,
and leaving a group out is a safe default rather than an omission.
## 2. Group family → canonical role
| `UG_WW_*` family | Canonical | Confidence |
|---|---|---|
| `APP_IDE_RO_<env>`, `BTMGR_RO_<env>` | **Viewer** | firm — `RO` is unambiguous |
| `<SITE>_<AREA>_OPERATOR` | **Operator** | firm |
| `BTMGR_<PLANT>_<env>` | **Operator** | firm — batch execution is operate-level |
| `<SITE>_<AREA>_ENGINEER` | **Engineer** | firm canonically, **but no app implements Engineer** — see §6.1 |
| `APP_IDE_<env>` | **Designer + Deployer** | **DECIDED** — see §2.1 |
| `APP_ADMIN_<env>` | **Administrator** | firm |
| `HIST_ADMIN_<env>` | **Administrator**, historian-scoped | firm for HistorianGateway; see §4 for the others |
| `SITE_ADMIN_<SITE>` | **Administrator** (scoped) — *not* Deployer | ⚠ DECIDE — §6.2 |
| `<SITE>_<AREA>_SUPERVISOR` | **Verifier**? | ⚠ DECIDE — §6.3 |
| `<SITE>_SMS_TESTER` | — | ⚠ DECIDE — §6.4 |
| `CITRIX_<env>` | **none — deliberately unmapped** | firm — see below |
**`CITRIX_*` is not a role and must not be mapped.** It grants desktop/session access to a
Citrix farm, not authority within an application. Mapping it would hand app privileges to
everyone who merely has a published desktop — 12 people in QA alone — which is a
privilege-escalation path disguised as a convenience.
### 2.1 `APP_IDE` carries both `Designer` and `Deployer` — DECIDED
The ArchestrA IDE is both the authoring surface *and* the deployment surface: templates are
created there and pushed to runtime from the same tool, by the same person, in the same
session. So `APP_IDE_<env>` maps to **both** canonical roles, and `APP_IDE_RO_<env>` maps to
**Viewer** — read the Galaxy, change nothing, deploy nothing.
This closes §6.5: the `AUTHOR`/`DEPLOY` separation the canonical set describes **does not
exist in this estate**, and rather than inventing one, the mapping reflects how the tooling
actually works. It is a deliberate collapse, not an oversight.
**Only ScadaBridge can express it.** Its `LdapGroupMapping` is a row per *(group, role)*
pair and its `RoleMapper` accumulates matches into a `HashSet<string>`, so two rows against
the same group grant both roles. The other three apps bind `GroupToRole` as a
`Dictionary<string, string>` — **one role per group, structurally; a duplicate key is a
config error, not a second grant.** That is not a limitation in practice: none of those
three implements `Deployer` at all, so there is nothing to lose.
**Consequence for `SITE_ADMIN_*`:** §6.2 previously recommended mapping it to a site-scoped
`Deployer`. With `Deployer` now sourced from `APP_IDE`, that would mean two unrelated groups
granting the same authority by different routes, and would leave "site admin" meaning
nothing beyond what an IDE user already has. `Administrator` is the remaining reading — see
the revised §6.2, which raises rather than settles the blast-radius question.
## 3. The environment axis is per-deployment, not per-role
`DEV`/`QA`/`PROD` is **orthogonal to role** and has no representation in `CanonicalRole`.
Do not encode it into role names; that triples the role set and buys nothing.
Instead, **each deployment maps only its own environment's groups.** The mapping already
lives in per-instance config, so this is free:
- the PROD instance's config lists `UG_WW_APP_ADMIN_PROD` and never mentions `_DEV`
- the DEV instance's config lists `UG_WW_APP_ADMIN_DEV`
This is load-bearing, not cosmetic. A single config listing all three environments would
make every DEV administrator a **PROD** administrator, silently, with no error anywhere.
The blocks in §4 are written for **PROD**. For DEV/QA, substitute the suffix. Note
`CITRIX` has no `PROD` group at all, which is further reason not to treat it as a role.
## 4. Per-app configuration
### OtOpcUa — `Security:Ldap:GroupToRole`
```json
"GroupToRole": {
"UG_WW_APP_ADMIN_PROD": "Administrator",
"UG_WW_SITE_ADMIN_WW19": "Administrator",
"UG_WW_APP_IDE_PROD": "Designer",
"UG_WW_APP_IDE_RO_PROD": "Viewer",
"UG_WW_HIST_ADMIN_PROD": "Viewer"
}
```
`HIST_ADMIN` gets `Viewer` because historization *targets* are authored in the OtOpcUa
AdminUI: a historian admin needs to see how tags are historized without being able to
change the address space. Plant-floor operator and engineer groups are **deliberately
absent** — the AdminUI is a configuration surface, and their runtime access is API-key
based (§1).
Replace `WW19` with the site that owns the instance; see §6.2 before listing all eight.
### ScadaBridge — `LdapGroupMapping` table
The only app with a role set rich enough to use most of the directory, and the only one
where site scoping is real.
| Group | Role | Scope |
|---|---|---|
| `UG_WW_APP_ADMIN_PROD` | `Administrator` | — |
| `UG_WW_APP_IDE_PROD` | `Designer` **and** `Deployer` — two rows (§2.1) | — |
| `UG_WW_APP_IDE_RO_PROD` | `Viewer` | — |
| `UG_WW_BTMGR_RO_PROD` | `Viewer` | — |
| `UG_WW_SITE_ADMIN_<SITE>` | `Administrator` | that site ⚠ §6.2 |
| `UG_WW_B19_SMS_OPERATOR` | `Operator` | B19 |
| `UG_WW_B19_VitE_OPERATOR` | `Operator` | B19 |
| `UG_WW_PON_PRESS_OPERATOR` | `Operator` | PON |
| `UG_WW_WN_ARCOM_OPERATOR` | `Operator` | WN |
| `UG_WW_WN_ARCOMXL_OPERATOR` | `Operator` | WN |
| `UG_WW_WN_SOLAR_OPERATOR` | `Operator` | WN |
| `UG_WW_WN_STERIS_OPERATOR` | `Operator` | WN |
| `UG_WW_BTMGR_BE_PROD` | `Operator` | BE |
| `UG_WW_BTMGR_MC_PROD` | `Operator` | MC |
| `UG_WW_WN_STERIS_SUPERVISOR` | `Verifier`? | WN ⚠ §6.3 |
| `UG_WW_B19_VitE_ENGINEER` | ⚠ §6.1 | B19 |
| `UG_WW_PON_PRESS_ENGINEER` | ⚠ §6.1 | PON |
The IDE grant is **two rows against one group** — this is the only place in the four apps
where that is possible, and it is the row pair most likely to be entered as one:
```sql
INSERT INTO LdapGroupMapping (LdapGroupName, Role) VALUES
('UG_WW_APP_IDE_PROD', 'Designer'),
('UG_WW_APP_IDE_PROD', 'Deployer');
```
`RoleMapper` unions every matched row into a `HashSet<string>`, so the user comes out
holding both. A single row grants only what it names — there is no implicit widening, and
nothing will report the missing half.
### MxAccessGateway — `MxGateway:Dashboard:GroupToRole`
```json
"GroupToRole": {
"UG_WW_APP_ADMIN_PROD": "Administrator",
"UG_WW_APP_IDE_PROD": "Viewer",
"UG_WW_APP_IDE_RO_PROD": "Viewer"
}
```
Only two roles exist, so `Designer` collapses to `Viewer`: an IDE author has a legitimate
reason to browse Galaxy through the gateway dashboard, but no reason to administer API
keys. The gRPC surface is API-key authenticated and is **not** governed by this mapping.
### HistorianGateway — `Dashboard:GroupToRole`
```json
"GroupToRole": {
"UG_WW_HIST_ADMIN_PROD": "Administrator",
"UG_WW_APP_ADMIN_PROD": "Administrator",
"UG_WW_APP_IDE_RO_PROD": "Viewer",
"UG_WW_BTMGR_RO_PROD": "Viewer"
}
```
`HIST_ADMIN` is the exact-fit group for this app — it is the one place in the directory
whose name already means what the app's `Administrator` grants.
## 5. Blast radius
Worth doing the arithmetic before adopting, because these groups are larger than they look:
| Grant | People |
|---|--:|
| `APP_ADMIN_PROD` → Administrator | 5 |
| `SITE_ADMIN_*` → Administrator, **all 8 sites** | **~48** |
| `APP_IDE_PROD` → Designer | 4 |
| all `_OPERATOR` groups → Operator | ~87 |
Mapping all eight `SITE_ADMIN_*` groups to `Administrator` on all four apps grants roughly
**48 people full administrative control of every app**, including the ones whose site has
nothing to do with the deployment. That is the single largest risk in this document, and
it is why §6.2 exists.
## 6. Open questions — DECIDE before adopting
### 6.1 `Engineer` has no home ⚠
`_ENGINEER` maps cleanly to canonical **Engineer** (`OBSERVE` + `OPERATE` + `TUNE`), and
**no app implements it.** OtOpcUa has `Viewer`/`Designer`/`Administrator`; ScadaBridge has
`Operator`/`Verifier` but no Engineer; the two gateways have `Viewer`/`Administrator`.
Options: (a) map `_ENGINEER` → `Operator` and accept that engineers lose `TUNE`;
(b) add an `Engineer` role to ScadaBridge; (c) leave unmapped, so the 18 people in the two
`_ENGINEER` groups get nothing. **Recommendation: (a) for now** — it is the honest subset,
grants no authority the group name does not imply, and can be widened later. Do not
silently map Engineer to Administrator to "cover" `TUNE`.
### 6.2 `SITE_ADMIN_*` — how much authority, and how many sites? ⚠
**Narrowed by §2.1, not settled.** `Deployer` now comes from `APP_IDE`, so site-scoped
`Deployer` is no longer available as the gentle reading of "site admin". What remains is
`Administrator` (scoped to the site), `Viewer`, or leaving it unmapped — and those are very
far apart.
This matters more than it did. There are **eight** of these groups at 6 members each, so
`Administrator` is the maximum grant in the vocabulary being handed to as many as ~48
people (§5). If a "site admin" is really a local IT contact rather than an application
owner, `Administrator` is badly wrong.
Second question, unchanged: **which sites should a given deployment list?** Every group has
exactly 6 members and they look like the same handful of people, but that is inference from
counts, not fact. **Recommendation: list only the owning site**, and confirm what the role
means before granting `Administrator` to any of them.
### 6.3 `_SUPERVISOR` → `Verifier`? ⚠
`UG_WW_WN_STERIS_SUPERVISOR` (6 members) is the only `_SUPERVISOR` group. ScadaBridge's
`Verifier` **approves** a two-person Secured Write that an `Operator` initiates, so a
supervisor approving an operator's action is a plausible reading — but "supervisor" could
equally mean a shift lead with no approval authority at all. Confirm with the group owner.
**Whatever is decided, never map one group to both `Operator` and `Verifier`.** The control
requires two *distinct* principals; a person holding both could initiate and approve their
own write, which removes the entire point of the mechanism while leaving it looking wired.
### 6.4 `_TESTER` ⚠
`UG_WW_B19_SMS_TESTER` (13 members) has no canonical equivalent and appears only in a QA
context. Leave unmapped until someone states what it is for — 13 people is too many to
guess at.
### 6.5 `Deployer` vs `AUTHOR` separation — RESOLVED
Closed by §2.1: `APP_IDE` grants both `Designer` and `Deployer`, because the ArchestrA IDE
authors and deploys from the same tool and the same session. The canonical `AUTHOR`/`DEPLOY`
split has no counterpart in this estate, and the mapping now records that as a decision
rather than leaving it to be discovered later. Same class of accepted SoD collapse already
noted in `CANONICAL-ROLES.md`.
## 7. Prerequisites
1. **Auth `0.2.1` adopted — DONE 2026-08-13.** All four apps pin `0.2.1` (OtOpcUa
`master @ 08e33fc7`, ScadaBridge `main @ d28824d9`, MxAccessGateway `main @ 1cb14d2`,
HistorianGateway `e42ff31`, merged to `main`; all pushed to origin).
The continuation-referral defect ([`ldap_setup.md`](ldap_setup.md)) is closed at the
app level, so this mapping is exercisable once configs are authored.
2. **`UG_WW_BTMGR_RO_QA` is empty.** It appears in the QA variant of the ScadaBridge and
HistorianGateway tables; mapping it grants nobody anything today. Harmless, but do not
read a working config as evidence that the grant works.
3. **Verify with a real account before rollout.** Every group in §4 was observed on one
long-tenured architect account, which holds every admin-flavoured group and **no**
operator group. The operator mappings are therefore untested against a real operator.
`ldapprobe --self-bind <operator-account>` settles it in one run — and specifically
whether such an account has any `memberOf` at all, since the library refuses a
zero-group login (`GroupLookupFailed`) regardless of a correct password.
4. **New groups are provisioned by ServiceNow ticket** (the group descriptions are
`TASK…` ids). If §6 concludes that a group is missing — a `Verifier` counterpart, or an
`Engineer` tier — that is a ticket, not a config change.