Standardize the dashboard role VALUE on the canonical six: Admin→Administrator (Viewer unchanged). Pure value rename via DashboardRoles.Admin constant + appsettings GroupToRole; the GatewayOptionsValidator allowed-set/message track the constant so they now require 'Administrator' or 'Viewer'. Enforcement is unchanged — Administrator authorizes exactly what Admin did. Dashboard roles are derived at login from LDAP groups via GroupToRole and are never persisted to the SQLite auth store, so no DB migration/seed change. UNTOUCHED: the separate gRPC API-key scope GatewayScopes.Admin = "admin" (lowercase) and every "admin" scope literal — a distinct data-plane system.
11 KiB
GLAuth — LDAP authn reference for mxaccessgw
GLAuth is a lightweight LDAP server installed on this dev box at
C:\publish\glauth\ and run as a Windows service via NSSM. It already
backs the LmxOpcUa OPC UA server's UserName-token authn and the LmxOpcUa
Admin UI's cookie login; this doc captures everything mxaccessgw needs
to consume the same directory so a single set of dev credentials covers
both stacks.
The authoritative copy of LmxOpcUa's reference lives at
C:\publish\glauth\auth.md. This doc is a redistilled view tailored to
mxaccessgw — what users + groups are already provisioned, how to bind
against them, and what's needed to add a gw-specific role.
Connection details
| Setting | Value |
|---|---|
| Protocol | LDAP (unencrypted) |
| Host | localhost |
| Port | 3893 |
| LDAPS | disabled in dev (set [ldaps] block to enable) |
| Base DN | dc=zb,dc=local |
| Bind DN format | cn={username},dc=zb,dc=local |
| Group OU | ou=<groupname>,ou=groups,dc=zb,dc=local |
| Failed-bind throttle | 3 fails → 10-minute IP lockout (per [behaviors]) |
Pre-existing groups (LmxOpcUa role taxonomy)
These map cleanly onto MxAccess capability boundaries — mxaccessgw should reuse them rather than define parallel groups so an operator with LmxOpcUa write rights doesn't need a second account for the gw.
| Group | GID | DN | LmxOpcUa meaning | Suggested mxgw mapping |
|---|---|---|---|---|
| ReadOnly | 5501 | ou=ReadOnly,ou=groups,dc=zb,dc=local |
Browse + read OPC UA nodes | Browse + Subscribe (read paths only) |
| WriteOperate | 5502 | ou=WriteOperate,ou=groups,dc=zb,dc=local |
Write FreeAccess / Operate attrs | Write (plain) |
| WriteTune | 5504 | ou=WriteTune,ou=groups,dc=zb,dc=local |
Write Tune attrs | WriteSecured (Tune only) |
| WriteConfigure | 5505 | ou=WriteConfigure,ou=groups,dc=zb,dc=local |
Write Configure attrs | WriteSecured (Configure) |
| AlarmAck | 5503 | ou=AlarmAck,ou=groups,dc=zb,dc=local |
Acknowledge alarms | gw alarm-ack RPC, when added |
A user can be in multiple groups — othergroups = [...] in the
config is a list. admin is the canonical example (in every role
group below).
Pre-provisioned users
| Username | Password | UID | Primary group | Other groups | Capabilities |
|---|---|---|---|---|---|
readonly |
readonly123 |
5001 | ReadOnly | — | Browse, read |
writeop |
writeop123 |
5002 | WriteOperate | — | + plain Write |
writetune |
writetune123 |
5005 | WriteTune | — | + WriteSecured (Tune) |
writeconfig |
writeconfig123 |
5006 | WriteConfigure | — | + WriteSecured (Configure) |
alarmack |
alarmack123 |
5003 | AlarmAck | — | Alarm acknowledgment |
admin |
admin123 |
5004 | ReadOnly | WriteOperate, AlarmAck, WriteTune, WriteConfigure | All roles |
serviceaccount |
serviceaccount123 |
5999 | ReadOnly | — | LDAP search capability (for bind-then-search) |
For mxaccessgw dev, admin covers every gw-side capability test;
readonly is the right "negative" case for proving Browse-OK /
Write-denied.
The gateway dashboard adds one role beyond this LmxOpcUa taxonomy:
GwAdmin. LdapOptions.RequiredGroup defaults to GwAdmin, so the
dashboard login and DashboardLdapLiveTests require admin to be a
member of a GwAdmin group. GwAdmin is not in the baseline
GLAuth config — it must be provisioned before dashboard authn or the
LDAP live tests work. See Provisioning the GwAdmin
group below.
Dashboard role value (Task 1.7): the LDAP
GwAdmingroup now maps to the canonical dashboard roleAdministrator(wasAdmin);GwReadermaps toViewer. This is a pure value rename viaMxGateway:Dashboard:GroupToRole— same operations are authorized. (This dashboard role is distinct from the lowercase gRPCadminAPI-key scope.)
Two bind patterns
1. Direct bind (simplest)
DN: cn=admin,dc=zb,dc=local
Password: admin123
Construct the DN from the username; bind. Works on GLAuth because
backend.nameformat = "cn" and groupformat = "ou" are set in the
config. Doesn't translate to Active Directory — AD users are keyed
by sAMAccountName, not cn. Use this only for dev convenience.
2. Bind-then-search (production-grade)
1. Bind as the service account (cn=serviceaccount,dc=zb,dc=local
/ serviceaccount123).
2. Search under dc=zb,dc=local with filter
(uid=<entered-username>) — or any attribute the deployment
identifies users by. GLAuth populates uid + cn.
3. Read the returned entry's DN + memberOf list (groups).
4. Bind again as the discovered DN with the entered password. If that
succeeds, authn passes; the memberOf values become the role set.
The second bind is the actual password check — the search is just a DN
discovery. This is the AD-friendly path: AD's
tokenGroups / LDAP_MATCHING_RULE_IN_CHAIN flatten nested groups, but
that's an enhancement, not required for first-pass dev.
LmxOpcUa's Server/Security/LdapUserAuthenticator.cs ships a working
implementation of this pattern using Novell.Directory.Ldap.NETStandard
v3.6.0 — copy the bind-then-search loop from there if mxaccessgw wants
to avoid re-deriving the LDAP escape-string handling.
Suggested mxgw configuration shape
A YAML/JSON section for mxaccessgw that mirrors LmxOpcUa's LdapOptions
record:
ldap:
enabled: true
server: localhost
port: 3893
useTls: false
allowInsecureLdap: true # dev only
searchBase: "dc=zb,dc=local"
serviceAccountDn: "cn=serviceaccount,dc=zb,dc=local"
serviceAccountPassword: "serviceaccount123"
userNameAttribute: "uid" # GLAuth populates this; AD uses sAMAccountName
displayNameAttribute: "cn"
groupAttribute: "memberOf"
groupToRole:
ReadOnly: "Browse"
WriteOperate: "Write"
WriteTune: "WriteSecured"
WriteConfigure: "WriteSecured"
AlarmAck: "AlarmAck"
groupAttribute returns full DNs like
ou=ReadOnly,ou=groups,dc=zb,dc=local — the authenticator
should strip the leading ou= (or cn= against AD) RDN value and
look that up in groupToRole.
Provisioning the GwAdmin group
GwAdmin is the gateway-specific dashboard-admin role. It is the
default LdapOptions.RequiredGroup, so the dashboard cookie login and
DashboardLdapLiveTests (MXGATEWAY_RUN_LIVE_LDAP_TESTS=1) reject
admin until a GwAdmin group exists and admin is a member.
GLAuth's baseline config ships only the five LmxOpcUa role groups, so
GwAdmin must be added to GLAuth rather than run from a separate LDAP
server:
- Edit
C:\publish\glauth\glauth.cfg - Append the group:
[[groups]]
name = "GwAdmin"
gidnumber = 5510 # pick the next free GID
- Add
5510toadmin'sothergroupslist soadminresolves theGwAdminrole. Add it to any other user that needs dashboard-admin rights. Or create a dedicated user:
[[users]]
name = "gwadmin"
givenname = "Gateway"
sn = "Admin"
mail = "gwadmin@lmxopcua.local"
uidnumber = 5010
primarygroup = 5510
passsha256 = "<sha256 of the password — see below>"
nssm restart GLAuth
After the restart, admin's memberOf includes
ou=GwAdmin,ou=groups,dc=zb,dc=local, which the authenticator
strips to GwAdmin and matches against RequiredGroup. The same
pattern applies to any future permission that doesn't fit the existing
five roles.
Generate passsha256 from a plaintext password:
# Windows / PowerShell
$bytes = [System.Text.Encoding]::UTF8.GetBytes("yourpassword")
$hash = [System.Security.Cryptography.SHA256]::Create().ComputeHash($bytes)
-join ($hash | ForEach-Object { $_.ToString("x2") })
# WSL / git-bash
echo -n "yourpassword" | openssl dgst -sha256
Quick verification
From mxaccessgw's dev box, prove the directory is reachable:
# Plain bind via PowerShell + System.DirectoryServices.Protocols
$ldap = New-Object System.DirectoryServices.Protocols.LdapConnection("localhost:3893")
$ldap.AuthType = [System.DirectoryServices.Protocols.AuthType]::Basic
$ldap.SessionOptions.ProtocolVersion = 3
$ldap.SessionOptions.SecureSocketLayer = $false
$cred = New-Object System.Net.NetworkCredential("cn=admin,dc=zb,dc=local","admin123")
$ldap.Bind($cred)
"Bind OK"
Or via ldapsearch if you have OpenLDAP CLI tools:
ldapsearch -x -H ldap://localhost:3893 \
-D "cn=admin,dc=zb,dc=local" -w admin123 \
-b "dc=zb,dc=local" "(uid=admin)"
The response should list admin's entry with memberOf populated for
all five role groups — plus GwAdmin once the gateway-specific group
is provisioned.
Service management
# Status / start / stop / restart
nssm status GLAuth
nssm start GLAuth
nssm stop GLAuth
nssm restart GLAuth
# Inspect what NSSM was told to launch
nssm get GLAuth Parameters
Logs:
| File | Purpose |
|---|---|
C:\publish\glauth\logs\stdout.log |
Bind events, search responses |
C:\publish\glauth\logs\stderr.log |
Startup errors, config parse failures |
After editing glauth.cfg, always tail stderr.log after the restart
to catch a fat-fingered TOML before it bites at first bind:
nssm restart GLAuth
Get-Content C:\publish\glauth\logs\stderr.log -Tail 20 -Wait
Active Directory migration cheat-sheet
LmxOpcUa's LdapOptions xml-doc captures the AD overrides; same set
applies to mxaccessgw verbatim. Keys that change:
| Field | GLAuth dev value | AD production value |
|---|---|---|
Server |
localhost |
a domain controller FQDN, or the domain itself |
Port |
3893 |
636 (LDAPS) — AD increasingly rejects plain bind under LDAP-signing enforcement |
UseTls |
false |
true |
AllowInsecureLdap |
true |
false |
SearchBase |
dc=zb,dc=local |
DC=corp,DC=example,DC=com |
ServiceAccountDn |
cn=serviceaccount,dc=zb,dc=local |
CN=MxGwSvc,OU=Service Accounts,DC=corp,... |
UserNameAttribute |
uid |
sAMAccountName (or userPrincipalName) |
GroupAttribute |
memberOf (unchanged) |
memberOf (unchanged) |
memberOf returns full DNs; the authenticator strips the leading
CN= value and uses it as the lookup key in groupToRole. Nested
groups are not auto-expanded; either flatten in the directory or
add a tokenGroups query as an enhancement.
Security notes for production
- Plaintext passwords in
glauth.cfgare dev-only. The config is unencrypted on disk; anyone with read access toC:\publish\glauth\can SHA256-rainbow-table the entries. Treat the dev creds as throwaway. Production LDAP is Active Directory. - The 3-fail / 10-minute lockout is per source IP, not per user — a
shared NAT can lock out a whole office. Tunable in
[behaviors]. - LDAPS isn't enabled in dev; binding sends passwords cleartext on the
wire. Fine for
localhost, never expose port 3893 off-box without enabling TLS first.