bc55396334
IntegrationTests-001: documented the live Galaxy Repository test suite and its MXGATEWAY_RUN_LIVE_GALAXY_TESTS / MXGATEWAY_LIVE_GALAXY_CONN gating in docs/GatewayTesting.md. IntegrationTests-002: documented the live LDAP test suite in docs/GatewayTesting.md and added a concrete "Provisioning the GwAdmin group" step to glauth.md so DashboardLdapLiveTests' GwAdmin-membership assumption is reproducible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
281 lines
10 KiB
Markdown
281 lines
10 KiB
Markdown
# 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.
|
|
|
|
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](#provisioning-the-gwadmin-group) below.
|
|
|
|
## 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:
|
|
|
|
```yaml
|
|
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`.
|
|
|
|
## 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:
|
|
|
|
1. Edit `C:\publish\glauth\glauth.cfg`
|
|
2. Append the group:
|
|
|
|
```toml
|
|
[[groups]]
|
|
name = "GwAdmin"
|
|
gidnumber = 5510 # pick the next free GID
|
|
```
|
|
|
|
3. Add `5510` to `admin`'s `othergroups` list so `admin` resolves the
|
|
`GwAdmin` role. Add it to any other user that needs dashboard-admin
|
|
rights. Or create a dedicated user:
|
|
|
|
```toml
|
|
[[users]]
|
|
name = "gwadmin"
|
|
givenname = "Gateway"
|
|
sn = "Admin"
|
|
mail = "gwadmin@lmxopcua.local"
|
|
uidnumber = 5010
|
|
primarygroup = 5510
|
|
passsha256 = "<sha256 of the password — see below>"
|
|
```
|
|
|
|
4. `nssm restart GLAuth`
|
|
|
|
After the restart, `admin`'s `memberOf` includes
|
|
`ou=GwAdmin,ou=groups,dc=lmxopcua,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:
|
|
|
|
```powershell
|
|
# Windows / PowerShell
|
|
$bytes = [System.Text.Encoding]::UTF8.GetBytes("yourpassword")
|
|
$hash = [System.Security.Cryptography.SHA256]::Create().ComputeHash($bytes)
|
|
-join ($hash | ForEach-Object { $_.ToString("x2") })
|
|
```
|
|
|
|
```bash
|
|
# WSL / git-bash
|
|
echo -n "yourpassword" | openssl dgst -sha256
|
|
```
|
|
|
|
## Quick verification
|
|
|
|
From mxaccessgw's dev box, prove the directory is reachable:
|
|
|
|
```powershell
|
|
# 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:
|
|
|
|
```bash
|
|
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 — plus `GwAdmin` once the gateway-specific group
|
|
is provisioned.
|
|
|
|
## Service management
|
|
|
|
```powershell
|
|
# 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:
|
|
|
|
```powershell
|
|
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.cfg` are dev-only.** The config is
|
|
unencrypted on disk; anyone with read access to `C:\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.
|