Remove static Users auth, use shared QualityMapper for historian, simplify LDAP permission checks

- Remove ConfigUserAuthenticationProvider and Users property — LDAP is the only auth mechanism
- Fix historian quality mapping to use existing QualityMapper (OPC DA quality bytes, not custom mapping)
- Add AppRoles constants, unify HasWritePermission/HasAlarmAckPermission into shared HasRole helper
- Hoist write permission check out of per-item loop, eliminate redundant _ldapRolesEnabled field
- Update docs (Configuration.md, Security.md, OpcUaServer.md, HistoricalDataAccess.md)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-28 19:23:20 -04:00
parent 74107ea95e
commit d9463d6998
19 changed files with 93 additions and 273 deletions

View File

@@ -114,13 +114,10 @@ Controls user authentication and write authorization for the OPC UA server. Defi
|----------|------|---------|-------------|
| `AllowAnonymous` | `bool` | `true` | Accepts anonymous client connections when `true` |
| `AnonymousCanWrite` | `bool` | `true` | Permits anonymous users to write when `true` |
| `Users` | `List<UserCredential>` | `[]` | List of username/password credentials for `UserName` token authentication |
Each entry in the `Users` list has two properties: `Username` (string) and `Password` (string). The `Users` list is ignored when `Ldap.Enabled` is `true`.
#### LDAP Authentication
When `Ldap.Enabled` is `true`, credentials are validated against the configured LDAP server and group membership determines OPC UA permissions. The `Users` list is ignored.
When `Ldap.Enabled` is `true`, credentials are validated against the configured LDAP server and group membership determines OPC UA permissions.
| Property | Type | Default | Description |
|----------|------|---------|-------------|
@@ -148,13 +145,12 @@ When LDAP is enabled, authenticated users receive permissions based on their LDA
Users can belong to multiple groups. The `admin` user in the default GLAuth configuration belongs to all three groups.
Example with LDAP authentication:
Example configuration:
```json
"Authentication": {
"AllowAnonymous": true,
"AnonymousCanWrite": false,
"Users": [],
"Ldap": {
"Enabled": true,
"Host": "localhost",
@@ -171,19 +167,6 @@ Example with LDAP authentication:
}
```
Example with static user list (no LDAP):
```json
"Authentication": {
"AllowAnonymous": true,
"AnonymousCanWrite": false,
"Users": [
{ "Username": "operator", "Password": "op123" },
{ "Username": "engineer", "Password": "eng456" }
]
}
```
### Security
Controls OPC UA transport security profiles and certificate handling. Defined in `SecurityProfileConfiguration`. See [Security Guide](security.md) for detailed usage.
@@ -321,7 +304,9 @@ Integration tests use this constructor to inject substitute implementations of `
"Authentication": {
"AllowAnonymous": true,
"AnonymousCanWrite": true,
"Users": []
"Ldap": {
"Enabled": false
}
},
"Security": {
"Profiles": ["None"],

View File

@@ -44,7 +44,7 @@ The `TOP` clause is included only when `maxValues > 0` (the OPC UA client specif
- `Value` column (double) takes priority over `vValue` (string). If both are null, the value is null.
- `SourceTimestamp` and `ServerTimestamp` are both set to the `DateTime` column.
- `StatusCode` is mapped from the Historian `Quality` byte via `MapQuality`.
- `StatusCode` is mapped from the Historian `Quality` byte via `QualityMapper` (the same OPC DA quality byte mapping used for live MXAccess data).
## Aggregate Reads
@@ -65,16 +65,15 @@ Null aggregate values return `BadNoData` status rather than `Good` with a null v
## Quality Mapping
`MapQuality` converts Wonderware Historian quality bytes to OPC UA status codes:
The Historian stores standard OPC DA quality bytes, the same format used by MXAccess at runtime. The quality byte is passed through the shared `QualityMapper` pipeline (`MapFromMxAccessQuality``MapToOpcUaStatusCode`), which maps the OPC DA quality families to OPC UA status codes:
| Historian Quality | OPC UA StatusCode |
|---|---|
| 0 | `Good` |
| 1 | `Bad` |
| 2-127 | `Bad` |
| 128+ | `Uncertain` |
| Historian Quality Byte | OPC DA Family | OPC UA StatusCode |
|---|---|---|
| 0-63 | Bad | `Bad` (with sub-code when an exact enum match exists) |
| 64-191 | Uncertain | `Uncertain` (with sub-code when an exact enum match exists) |
| 192+ | Good | `Good` (with sub-code when an exact enum match exists) |
This follows the Wonderware convention where quality 0 indicates a good sample, 1 indicates explicitly bad data, and values at or above 128 represent uncertain quality (e.g., interpolated or suspect values).
See `Domain/QualityMapper.cs` and `Domain/Quality.cs` for the full mapping table and sub-code definitions.
## Aggregate Function Mapping

View File

@@ -63,15 +63,15 @@ The `ServiceLevel` is updated whenever MXAccess connection state changes or Gala
`UserTokenPolicies` are dynamically configured based on the `Authentication` settings in `appsettings.json`:
- An `Anonymous` user token policy is added when `AllowAnonymous` is `true` (the default).
- A `UserName` user token policy is added when the `Users` list contains at least one entry.
- A `UserName` user token policy is added when `Ldap.Enabled` is `true`.
Both policies can be active simultaneously, allowing clients to connect with or without credentials.
### Session impersonation
When a client presents `UserName` credentials, the server validates them through `IUserAuthenticationProvider`. If the credentials do not match any entry in the configured `Users` list, the session is rejected.
When a client presents `UserName` credentials, the server validates them through `IUserAuthenticationProvider`. If LDAP authentication is enabled, credentials are validated via LDAP bind and group membership determines the user's application-level roles (`ReadOnly`, `ReadWrite`, `AlarmAck`). If validation fails, the session is rejected.
On successful validation, the session identity is set to a `RoleBasedIdentity` that carries the user's granted role IDs. Authenticated users receive the `WellKnownRole_AuthenticatedUser` role. Anonymous connections receive the `WellKnownRole_Anonymous` role. These roles are used downstream by the write override to enforce `AnonymousCanWrite` restrictions.
On successful validation, the session identity is set to a `RoleBasedIdentity` that carries the user's granted role IDs. Authenticated users receive the `WellKnownRole_AuthenticatedUser` role. Anonymous connections receive the `WellKnownRole_Anonymous` role. When LDAP is enabled, application-level roles from group membership control write and alarm-ack permissions. Without LDAP, `AnonymousCanWrite` controls whether anonymous users can write.
## Certificate handling

View File

@@ -147,19 +147,23 @@ Remove `None` from the `Profiles` list to prevent unencrypted connections:
}
```
### 3. Configure named users
### 3. Configure LDAP authentication
Disable anonymous access and define named users in the `Authentication` section. Use `AnonymousCanWrite` to control whether anonymous clients (if still allowed) can write:
Enable LDAP authentication to validate credentials against the GLAuth server. LDAP group membership controls what each user can do (read, write, alarm acknowledgment). See [Configuration Guide](Configuration.md) for the full LDAP property reference.
```json
{
"Authentication": {
"AllowAnonymous": false,
"AnonymousCanWrite": false,
"Users": [
{ "Username": "operator", "Password": "secure-password" },
{ "Username": "viewer", "Password": "read-only-password" }
]
"Ldap": {
"Enabled": true,
"Host": "localhost",
"Port": 3893,
"BaseDN": "dc=lmxopcua,dc=local",
"ServiceAccountDn": "cn=serviceaccount,dc=lmxopcua,dc=local",
"ServiceAccountPassword": "serviceaccount123"
}
}
}
```