# 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](#provisioning-the-gwadmin-group) below.

## 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:

```yaml
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:

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=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:

```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=zb,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=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

```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=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.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.
