Restyles the Blazor dashboard onto a portable token-based theme so it reads like an instrument panel: warm-paper background, hairline-ruled panels, IBM Plex type, monospace tabular numerics, and status carried by colour chips. Vendors theme.css + IBM Plex fonts, rewrites dashboard.css as a thin token-driven view layer, and swaps the Bootstrap navbar and status badges for the design-system app bar and chips. Also includes pending API-key management, Galaxy hierarchy projection, and constraint-enforcement work with their tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.5 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=lmxopcua,dc=local |
| Bind DN format | cn={username},dc=lmxopcua,dc=local |
| Group OU | ou=<groupname>,ou=groups,dc=lmxopcua,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=lmxopcua,dc=local |
Browse + read OPC UA nodes | Browse + Subscribe (read paths only) |
| WriteOperate | 5502 | ou=WriteOperate,ou=groups,dc=lmxopcua,dc=local |
Write FreeAccess / Operate attrs | Write (plain) |
| WriteTune | 5504 | ou=WriteTune,ou=groups,dc=lmxopcua,dc=local |
Write Tune attrs | WriteSecured (Tune only) |
| WriteConfigure | 5505 | ou=WriteConfigure,ou=groups,dc=lmxopcua,dc=local |
Write Configure attrs | WriteSecured (Configure) |
| AlarmAck | 5503 | ou=AlarmAck,ou=groups,dc=lmxopcua,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.
Two bind patterns
1. Direct bind (simplest)
DN: cn=admin,dc=lmxopcua,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=lmxopcua,dc=local
/ serviceaccount123).
2. Search under dc=lmxopcua,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=lmxopcua,dc=local"
serviceAccountDn: "cn=serviceaccount,dc=lmxopcua,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=lmxopcua,dc=local — the authenticator
should strip the leading ou= (or cn= against AD) RDN value and
look that up in groupToRole.
Adding a gw-specific group (when reuse isn't enough)
If mxaccessgw needs a permission that doesn't fit the existing five
roles (e.g. GwAdmin for shutdown/recycle commands), add it to
GLAuth rather than running a separate LDAP server:
- Edit
C:\publish\glauth\glauth.cfg - Append:
[[groups]]
name = "GwAdmin"
gidnumber = 5510 # pick the next free GID
- Add the group to whichever existing user(s) should have it via
othergroups = [..., 5510]. Or create a new 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
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=lmxopcua,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=lmxopcua,dc=local" -w admin123 \
-b "dc=lmxopcua,dc=local" "(uid=admin)"
The response should list admin's entry with memberOf populated for
all five role groups.
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=lmxopcua,dc=local |
DC=corp,DC=example,DC=com |
ServiceAccountDn |
cn=serviceaccount,dc=lmxopcua,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.