# `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_`, `BTMGR_RO_` | **Viewer** | firm — `RO` is unambiguous | | `__OPERATOR` | **Operator** | firm | | `BTMGR__` | **Operator** | firm — batch execution is operate-level | | `__ENGINEER` | **Engineer** | firm canonically, **but no app implements Engineer** — see §6.1 | | `APP_IDE_` | **Designer + Deployer** | **DECIDED** — see §2.1 | | `APP_ADMIN_` | **Administrator** | firm | | `HIST_ADMIN_` | **Administrator**, historian-scoped | firm for HistorianGateway; see §4 for the others | | `SITE_ADMIN_` | **Administrator** (scoped) — *not* Deployer | ⚠ DECIDE — §6.2 | | `__SUPERVISOR` | **Verifier**? | ⚠ DECIDE — §6.3 | | `_SMS_TESTER` | — | ⚠ DECIDE — §6.4 | | `CITRIX_` | **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_` maps to **both** canonical roles, and `APP_IDE_RO_` 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`, so two rows against the same group grant both roles. The other three apps bind `GroupToRole` as a `Dictionary` — **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_` | `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`, 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 ` 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.