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:
Joseph Doherty
2026-03-27 02:14:37 -04:00
parent b27d355763
commit bbd043e97b
24 changed files with 499 additions and 34 deletions

View File

@@ -192,6 +192,56 @@ Pass `AuthenticationConfiguration` and `IUserAuthenticationProvider` through:
- `OpcUaServerHost` uses `AllowAnonymous` and user count for `UserTokenPolicies`
- `LmxNodeManager` receives `AnonymousCanWrite` flag for write enforcement
### 7. CLI tool authentication — `tools/opcuacli-dotnet/`
Add `--username` and `--password` options to `OpcUaHelper.ConnectAsync` so all commands can authenticate.
**`OpcUaHelper.cs`** — update `ConnectAsync` to accept optional credentials:
```csharp
public static async Task<Session> ConnectAsync(string endpointUrl, string? username = null, string? password = null)
{
// ... existing config setup ...
UserIdentity identity = (username != null)
? new UserIdentity(username, password ?? "")
: new UserIdentity();
var session = await Session.Create(
config, configuredEndpoint, false,
"OpcUaCli", 60000, identity, null);
return session;
}
```
**Each command** — add shared options. Since CliFx doesn't support base classes for shared options, add `-U` / `-P` to each command:
```csharp
[CommandOption("username", 'U', Description = "Username for authentication")]
public string? Username { get; init; }
[CommandOption("password", 'P', Description = "Password for authentication")]
public string? Password { get; init; }
```
And pass to `OpcUaHelper.ConnectAsync(Url, Username, Password)`.
**Usage examples:**
```bash
# Anonymous (current behavior)
dotnet run -- read -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=1;s=TestMachine_001.MachineID"
# Authenticated
dotnet run -- read -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=1;s=TestMachine_001.MachineID" -U operator -P op123
# Write with credentials (when AnonymousCanWrite is false)
dotnet run -- write -u opc.tcp://localhost:4840/LmxOpcUa -n "ns=1;s=TestMachine_001.MachineID" -v "NEW" -U operator -P op123
```
**README update** — document the `-U` / `-P` flags.
## Files to Create/Modify
| File | Change |
@@ -207,7 +257,39 @@ Pass `AuthenticationConfiguration` and `IUserAuthenticationProvider` through:
| `src/.../appsettings.json` | Add Authentication section |
| `tests/.../Authentication/UserAuthenticationTests.cs` | NEW — credential validation tests |
| `tests/.../Integration/WriteAccessTests.cs` | NEW — anonymous vs authenticated write tests |
| `docs/Configuration.md` | Update with Authentication section |
| `tools/opcuacli-dotnet/OpcUaHelper.cs` | Add username/password parameters to `ConnectAsync` |
| `tools/opcuacli-dotnet/Commands/*.cs` | Add `-U` / `-P` options to all 7 commands |
| `tools/opcuacli-dotnet/README.md` | Document authentication flags |
| `docs/Configuration.md` | Add Authentication section with settings table |
| `docs/OpcUaServer.md` | Update security policy and UserTokenPolicies section |
| `docs/ReadWriteOperations.md` | Document role-based write enforcement |
| `docs/CliTool.md` | Document `-U` / `-P` authentication flags |
## Tests
### Unit tests — `tests/.../Authentication/UserAuthenticationTests.cs` (NEW)
- `ConfigUserAuthenticationProvider` validates correct username/password
- `ConfigUserAuthenticationProvider` rejects wrong password
- `ConfigUserAuthenticationProvider` rejects unknown username
- `ConfigUserAuthenticationProvider` is case-insensitive on username
- Empty user list rejects all credentials
- `AuthenticationConfiguration` defaults: `AllowAnonymous=true`, `AnonymousCanWrite=true`, empty Users list
### Integration tests — `tests/.../Integration/WriteAccessTests.cs` (NEW)
- Anonymous connect succeeds when `AllowAnonymous=true`
- Anonymous connect rejected when `AllowAnonymous=false` (requires test fixture to configure auth)
- Anonymous read succeeds regardless of `AnonymousCanWrite`
- Anonymous write succeeds when `AnonymousCanWrite=true`
- Anonymous write rejected with `BadUserAccessDenied` when `AnonymousCanWrite=false`
- Authenticated user write succeeds when `AnonymousCanWrite=false`
- Invalid credentials rejected with `BadUserAccessDenied`
### Existing test updates
- `OpcUaServerFixture` — add option to configure `AuthenticationConfiguration` for auth-aware test fixtures
- Existing write tests continue passing (default config allows anonymous writes)
## Verification