Add authentication and role-based write access control
Implements configurable user authentication (anonymous + username/password) with pluggable credential provider (IUserAuthenticationProvider). Anonymous writes can be disabled via AnonymousCanWrite setting while reads remain open. Adds -U/-P flags to all CLI commands for authenticated sessions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,23 @@ dotnet run -- <command> [options]
|
||||
|
||||
`OpcUaHelper.ConvertValue()` converts a raw string from the command line into the runtime type expected by the target node. It uses the current node value to infer the type (bool, byte, short, int, float, double, etc.) and falls back to string if the type is not recognized.
|
||||
|
||||
## Authentication Options
|
||||
|
||||
All commands accept optional credentials for `UserName` token authentication:
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-U` / `--username` | Username for OPC UA `UserName` token authentication |
|
||||
| `-P` / `--password` | Password for OPC UA `UserName` token authentication |
|
||||
|
||||
When `-U` and `-P` are provided, `OpcUaHelper.ConnectAsync()` passes a `UserIdentity(username, password)` to `Session.Create`. Without credentials, an anonymous `UserIdentity` is used.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
dotnet run -- write -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -v 42 -U operator -P op123
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### connect
|
||||
|
||||
@@ -21,6 +21,7 @@ configuration.GetSection("MxAccess").Bind(_config.MxAccess);
|
||||
configuration.GetSection("GalaxyRepository").Bind(_config.GalaxyRepository);
|
||||
configuration.GetSection("Dashboard").Bind(_config.Dashboard);
|
||||
configuration.GetSection("Historian").Bind(_config.Historian);
|
||||
configuration.GetSection("Authentication").Bind(_config.Authentication);
|
||||
```
|
||||
|
||||
This pattern uses `IConfiguration.GetSection().Bind()` rather than `IOptions<T>` because the service targets .NET Framework 4.8, where the full dependency injection container is not used.
|
||||
@@ -102,6 +103,33 @@ Controls the Wonderware Historian connection for OPC UA historical data access.
|
||||
| `CommandTimeoutSeconds` | `int` | `30` | SQL command timeout for historian queries |
|
||||
| `MaxValuesPerRead` | `int` | `10000` | Maximum values returned per `HistoryRead` request |
|
||||
|
||||
### Authentication
|
||||
|
||||
Controls user authentication and write authorization for the OPC UA server. Defined in `AuthenticationConfiguration`.
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `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 defaults preserve the existing behavior: anonymous clients can connect, read, and write with no credentials required. To restrict writes to authenticated users, set `AnonymousCanWrite` to `false` and add entries to the `Users` list.
|
||||
|
||||
Example configuration:
|
||||
|
||||
```json
|
||||
"Authentication": {
|
||||
"AllowAnonymous": true,
|
||||
"AnonymousCanWrite": false,
|
||||
"Users": [
|
||||
{ "Username": "operator", "Password": "op123" },
|
||||
{ "Username": "engineer", "Password": "eng456" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Feature Flags
|
||||
|
||||
Three boolean properties act as feature flags that control optional subsystems:
|
||||
@@ -177,6 +205,11 @@ Integration tests use this constructor to inject substitute implementations of `
|
||||
"ConnectionString": "Server=localhost;Database=Runtime;Integrated Security=true;",
|
||||
"CommandTimeoutSeconds": 30,
|
||||
"MaxValuesPerRead": 10000
|
||||
},
|
||||
"Authentication": {
|
||||
"AllowAnonymous": true,
|
||||
"AnonymousCanWrite": true,
|
||||
"Users": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -33,19 +33,33 @@ The configuration covers:
|
||||
|
||||
## Security Policy
|
||||
|
||||
The server runs with `MessageSecurityMode.None` and `SecurityPolicies.None`. Only anonymous user tokens are accepted:
|
||||
The server runs with `MessageSecurityMode.None` and `SecurityPolicies.None`:
|
||||
|
||||
```csharp
|
||||
SecurityPolicies = { new ServerSecurityPolicy
|
||||
{
|
||||
SecurityMode = MessageSecurityMode.None,
|
||||
SecurityPolicyUri = SecurityPolicies.None
|
||||
} },
|
||||
UserTokenPolicies = { new UserTokenPolicy(UserTokenType.Anonymous) }
|
||||
} }
|
||||
```
|
||||
|
||||
This is intentional for plant-floor deployments where the server sits on an isolated OT network. Galaxy-level security classification controls write access per attribute rather than at the transport layer.
|
||||
|
||||
### User token policies
|
||||
|
||||
`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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Certificate handling
|
||||
|
||||
On startup, `OpcUaServerHost.StartAsync` calls `CheckApplicationInstanceCertificate(false, 2048)` to locate or create a 2048-bit self-signed certificate. The certificate subject follows the format `CN={ServerName}, O=ZB MOM, DC=localhost`. Certificate stores use the directory-based store type under `%LOCALAPPDATA%\OPC Foundation\pki\`:
|
||||
|
||||
@@ -64,6 +64,17 @@ nextArray.SetValue(ConvertArrayElementValue(normalizedValue, elementType), index
|
||||
updatedArray = nextArray;
|
||||
```
|
||||
|
||||
### Role-based write enforcement
|
||||
|
||||
When `AnonymousCanWrite` is `false` in the `Authentication` configuration, the write override enforces role-based access control before dispatching to MXAccess. The check order is:
|
||||
|
||||
1. The base class `Write` runs first, enforcing `AccessLevel`. Nodes without `CurrentWrite` get `BadNotWritable` and the override skips them.
|
||||
2. The override checks whether the node is in the Galaxy namespace. Non-namespace nodes are skipped.
|
||||
3. If `AnonymousCanWrite` is `false`, the override inspects `context.OperationContext.Session` for `GrantedRoleIds`. If the session does not hold `WellKnownRole_AuthenticatedUser`, the error is set to `BadUserAccessDenied` and the write is rejected.
|
||||
4. If the role check passes (or `AnonymousCanWrite` is `true`), the write proceeds to MXAccess.
|
||||
|
||||
The existing security classification enforcement (ReadOnly nodes getting `BadNotWritable` via `AccessLevel`) still applies first and takes precedence over the role check.
|
||||
|
||||
## Value Type Conversion
|
||||
|
||||
`CreatePublishedDataValue` wraps the conversion pipeline. `NormalizePublishedValue` checks whether the tag is an array type with a declared `ArrayDimension` and substitutes a default typed array (via `CreateDefaultArrayValue`) when the raw value is null. This prevents OPC UA clients from receiving a null variant for array nodes, which violates the specification for nodes declared with `ValueRank.OneDimension`.
|
||||
|
||||
Reference in New Issue
Block a user