mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 06:31:35 +08:00
be50f02700
# Description Expose existing stack APIs through **337 additional domain MCP tools**: 114 industrial-companion tools, 35 WoT/xRegistry/document tools, and 188 higher-level stack-workflow tools. The fully enabled catalogue contains **492 tools**; profile selection is not an execution grant. The packages now follow the reviewed structure: **`Opc.Ua.Mcp.Workflows`** contains roles/users, files, alarms and history; **`Opc.Ua.Mcp.WotCon`** contains management/documents and independently registered live WoT execution. GDS and schema tools remain separate packages. The executable composes **18 libraries**. | Workflow | Added domain tools | Profiles | | --- | ---: | --- | | GDS directory, certificates, reference-only credentials/tokens and Part 21 onboarding | 46 | `gds`, `certificates`, `credentials`, `onboarding` | | Roles/users, regular and temporary files, alarms/conditions and historical access | 74 | `roles`, `users`, `files`, `alarms`, `history` | | Bounded runtime types, XSD/BSD/UA JSON, PubSub and REST OpenAPI artifacts | 6 | `schemas` | | Named PubSub transports, remote configuration, declarative adapters/bridges and SKS | 52 | `pubsub-runtime`, `pubsub-configuration`, `pubsub-bridge`, `pubsub-security` | | Live HTTP(S), MQTT, Modbus TCP and OPC UA WoT forms | 10 | `wot-runtime` | - Reuse typed clients, generated proxies, SDK transports, schema providers and injected services rather than introducing another session manager or protocol engine. - Require host enablement plus exact caller/session/secret/network grants. Protected generic Call/Write/node-management/history/file paths enforce the same policy. - Bind classifications, dispatches, file handles and historical cursors to actual UA generations. Recovery cannot retarget an old handle or authorized mutation. Preserve acknowledged results after late cancellation; do not imply atomicity or retry business commands. - Use opaque secret-store references, explicit writable-store registration, issuer/resource/caller binding and reserved capacity before issuance. Report remote success/local persistence failure as partial completion; redact provider-controlled diagnostic text. - Bound new named runtimes to eight per host, two per caller, five-minute idle expiry and a thirty-minute absolute lease by default. Pending/closing resources consume quota; failed cleanup remains visible. Credential invalidation and session recovery close dependent runtimes. - Keep document planning separate from execution, artifacts root-confined and real JSON inputs typed. Preserve old profile ordinals while widening the set to 64 bits. CI grades fully enabled privileged catalogues rather than connection-only views. **Limitations:** live Kafka remains explicitly unavailable because the backend cannot enforce policy on broker-discovered connections; configuration/schema/capability inspection stays available. The latest public Dekaf v1.23.5 was also checked and its DNS injection remains internal. No private-API workaround or bootstrap-only security claim was added. Full legacy Core registration retains its documented JSON-reflection limitation: native tests isolate its four connection descriptors and validate the expanded production registrations, not whole-host NativeAOT support. ## Related Issues - Fixes #4730. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed **all** PR feedback received.
344 lines
17 KiB
Markdown
344 lines
17 KiB
Markdown
# Role-Based Security (OPC UA Part 18)
|
|
|
|
## Contents
|
|
|
|
- [Overview](#overview)
|
|
- [Server side](#server-side)
|
|
- [`IRoleManager`](#irolemanager)
|
|
- [Authoring an identity-mapping rule with claims](#authoring-an-identity-mapping-rule-with-claims)
|
|
- [`RoleConfigurationChanged` event](#roleconfigurationchanged-event)
|
|
- [Typed-proxy address-space binding](#typed-proxy-address-space-binding)
|
|
- [Default impersonation flow](#default-impersonation-flow)
|
|
- [User Management](#user-management-part-18-5)
|
|
- [User name / password storage](#user-name--password-storage)
|
|
- [Client side](#client-side)
|
|
- [GDS](#gds)
|
|
- [Known limitations](#known-limitations)
|
|
- [References](#references)
|
|
|
|
## Overview
|
|
|
|
The OPC UA .NET Standard Stack implements the role-based security model from
|
|
[OPC UA Part 18: Role-Based Security (v1.05.06)](https://reference.opcfoundation.org/Core/Part18/v105/docs/)
|
|
together with the well-known roles defined in
|
|
[OPC UA Part 3 4.9.2](https://reference.opcfoundation.org/Core/Part3/v105/docs/4.9.2).
|
|
|
|
The nine well-known roles are pre-registered at server start:
|
|
|
|
| Role | NodeId | Reserved? | Default identities |
|
|
|------|--------|-----------|--------------------|
|
|
| `Anonymous` | `i=15644` | Yes | `Anonymous`, `AuthenticatedUser` |
|
|
| `AuthenticatedUser` | `i=15656` | Yes | `AuthenticatedUser` |
|
|
| `TrustedApplication` | `i=18625` | Yes | `TrustedApplication` |
|
|
| `Observer` | `i=15668` | No | (empty) |
|
|
| `Operator` | `i=15680` | No | (empty) |
|
|
| `Engineer` | `i=16036` | No | (empty) |
|
|
| `Supervisor` | `i=15692` | No | (empty) |
|
|
| `ConfigureAdmin` | `i=15716` | No | (empty) |
|
|
| `SecurityAdmin` | `i=15704` | No | (empty) |
|
|
|
|
Reserved roles cannot be modified or removed per Part 18 4.3 - any attempt to call `AddIdentity`, `AddApplication`, `AddEndpoint`, `RemoveIdentity`, `RemoveApplication`, `RemoveEndpoint`, or `RemoveRole` on them returns `Bad_RequestNotAllowed`.
|
|
|
|
See `src/Opc.Ua.Server/RoleBasedUserManagement/` for the source.
|
|
|
|
## Server side
|
|
|
|
### IRoleManager
|
|
|
|
The `IRoleManager` interface (`Opc.Ua.Server.IRoleManager`) is the server-side extensibility surface. The default implementation `Opc.Ua.Server.RoleManager` is fully in-memory; integrators that need persistence implement `IRoleManager` directly and inject the instance via `IServerInternal.SetRoleManager` before the server starts.
|
|
|
|
Every mutator returns a `ServiceResult` that mirrors the spec-defined status codes:
|
|
|
|
- `AddIdentity` (4.4.5) - Good or Bad_InvalidArgument / Bad_AlreadyExists / Bad_RequestNotAllowed / Bad_NodeIdUnknown.
|
|
- `RemoveIdentity` (4.4.6) - Good or Bad_NotFound / Bad_RequestNotAllowed / Bad_NodeIdUnknown.
|
|
- `AddApplication` (4.4.7) - Good or Bad_InvalidArgument / Bad_AlreadyExists / Bad_RequestNotAllowed / Bad_NodeIdUnknown.
|
|
- `RemoveApplication` (4.4.8) - Good or Bad_NotFound / Bad_RequestNotAllowed / Bad_NodeIdUnknown.
|
|
- `AddEndpoint` (4.4.9) - Good or Bad_InvalidArgument / Bad_AlreadyExists / Bad_RequestNotAllowed / Bad_NodeIdUnknown.
|
|
- `RemoveEndpoint` (4.4.10) - Good or Bad_NotFound / Bad_RequestNotAllowed / Bad_NodeIdUnknown.
|
|
- `AddRole` (4.2.2) - Good or Bad_InvalidArgument / Bad_AlreadyExists.
|
|
- `RemoveRole` (4.2.3) - Good or Bad_NodeIdUnknown / Bad_RequestNotAllowed.
|
|
- `SetApplicationsExclude` / `SetEndpointsExclude` / `SetCustomConfiguration` - Good or Bad_RequestNotAllowed / Bad_NodeIdUnknown.
|
|
|
|
`IdentityMappingRuleType` is validated per Part 18 4.4.3 before storage:
|
|
|
|
- `Anonymous`, `AuthenticatedUser`, `TrustedApplication` - criteria must be empty.
|
|
- `Thumbprint` - upper-case hexadecimal, no spaces.
|
|
- `X509Subject` - `Name=Value(/Name=Value)*` grammar with names from Part 18 Table 10.
|
|
|
|
`EndpointType` comparisons in identity resolution honour Part 18 4.4.2: fields set to their default value on the rule
|
|
side act as wildcards. A rule may therefore constrain any subset of endpoint fields by assigning non-default values to
|
|
those fields. For example, this rule applies to every endpoint URL that uses `SignAndEncrypt`:
|
|
|
|
```csharp
|
|
IRoleManager roles = server.CurrentInstance.RoleManager;
|
|
|
|
roles.AddEndpoint(
|
|
ObjectIds.WellKnownRole_Engineer,
|
|
new EndpointType
|
|
{
|
|
SecurityMode = MessageSecurityMode.SignAndEncrypt
|
|
});
|
|
```
|
|
|
|
At least one field must be non-default; `AddEndpoint` returns `Bad_InvalidArgument` for a null or completely default
|
|
`EndpointType`.
|
|
|
|
> **Identity-claim criteria support**:
|
|
> - `IdentityCriteriaType.GroupId` probes the returned `IUserIdentity` for `Opc.Ua.Identity.IIdentityClaims` (see [Identity Providers](IdentityProviders.md)) and matches `IIdentityClaims.Groups`.
|
|
> - `IdentityCriteriaType.Role` matches roles asserted **inside the access token** via `IIdentityClaims.Roles`, optionally prefixed by the issuer URI as `iss/roleName`, per Part 18 4.4.4.
|
|
|
|
|
|
### Authoring an identity-mapping rule with claims
|
|
|
|
JWT authenticators that return an identity implementing `IIdentityClaims`
|
|
allow Part 18 rules to match Entra-style claims. The in-box
|
|
`RoleManager` treats entries in a role's `Identities` collection as OR
|
|
criteria, per the standard `IdentityMappingRuleType[]` shape. Add one
|
|
entry for each accepted claim value:
|
|
|
|
```csharp
|
|
IRoleManager roles = server.CurrentInstance.RoleManager;
|
|
|
|
roles.AddIdentity(
|
|
ObjectIds.WellKnownRole_Engineer,
|
|
new IdentityMappingRuleType
|
|
{
|
|
CriteriaType = IdentityCriteriaType.Role,
|
|
Criteria = "https://login.microsoftonline.com/{tenant}/v2.0/OpcUa.Engineer"
|
|
});
|
|
|
|
roles.AddIdentity(
|
|
ObjectIds.WellKnownRole_Engineer,
|
|
new IdentityMappingRuleType
|
|
{
|
|
CriteriaType = IdentityCriteriaType.GroupId,
|
|
Criteria = "00000000-1111-2222-3333-444444444444"
|
|
});
|
|
```
|
|
|
|
The first rule matches an access-token role claim `OpcUa.Engineer` only
|
|
when `IIdentityClaims.Issuer` is the Entra issuer. The second matches a
|
|
`groups` claim containing the Entra group object id. If your policy must
|
|
require both a role and a group simultaneously, implement a custom
|
|
`IRoleManager` (or a custom authorization layer) because the shipped
|
|
`RoleConfigurationOptions` DTO does not define an AND-rule collection.
|
|
|
|
### RoleConfigurationChanged event
|
|
|
|
`IRoleManager.RoleConfigurationChanged` fires after every successful mutation with the role NodeId and a `RoleConfigurationChangeKind`. Integrators can subscribe to:
|
|
|
|
- materialize dynamic-role address-space nodes when `RoleAdded` fires (the default in-memory `RoleManager` does not create address-space nodes for `AddRole` - the well-known roles are already in the standard nodeset);
|
|
- re-evaluate active sessions per Part 18 4.4.1 ("If the configuration of a Role is changed, the Role assignment to active Session shall be re-evaluated and applied");
|
|
- emit audit events beyond the spec-defined `RoleMappingRuleChangedAuditEvent`.
|
|
|
|
### Typed-proxy address-space binding
|
|
|
|
`RoleStateBinding.BindAsync(diagnosticsNodeManager, roleManager, auditServer)` uses the source-generated typed proxies (`RoleSetState`, `RoleState`, `AddIdentityMethodState`, `AddRoleMethodState`, ...). Each typed `OnCallAsync` delegate:
|
|
|
|
1. Enforces `RoleAuthorizationGate.CheckAdmin` (SecurityAdmin role over a `SignAndEncrypt` channel) - returns `Bad_SecurityModeInsufficient` or `Bad_UserAccessDenied` otherwise (Part 18 4.2 / 4.4).
|
|
2. Delegates to `IRoleManager`.
|
|
3. Reports a `RoleMappingRuleChangedAuditEvent` (Part 18 4.5).
|
|
4. Keeps the typed `Identities`, `Applications`, `Endpoints`, `ApplicationsExclude`, `EndpointsExclude` and `CustomConfiguration` property values in sync with the manager via the `RoleConfigurationChanged` event.
|
|
|
|
`DiagnosticsNodeManager.AddBehaviourToPredefinedNodeAsync` upgrades passive `BaseObjectState` instances of `RoleSetType` and `RoleType` to the typed `RoleSetState` and `RoleState` proxies at predefined-node load time.
|
|
|
|
Queued removal retains the exact binding generation and node reference it owns.
|
|
Re-adding a role advances that generation, even when the same well-known NodeId
|
|
and RoleState are reused. Cleanup rechecks the role manager and conditionally
|
|
claims the removed generation before deleting the node. A failed AddRole that
|
|
never acquired a binding cannot delete the foreign node that caused its collision.
|
|
|
|
### Default impersonation flow
|
|
|
|
Per Part 3 4.9 and Part 18 4.3 the default `RoleManager` rules already grant:
|
|
|
|
- `Anonymous` for sessions without authentication;
|
|
- `AuthenticatedUser` for any session with a non-anonymous token;
|
|
- `TrustedApplication` when the session was authenticated with a trusted ApplicationInstance certificate over a signed channel.
|
|
|
|
Any nonempty `Applications` filter requires a signed channel and an
|
|
application certificate, including when `ApplicationsExclude` is true.
|
|
The `Thumbprint` and `X509Subject` identity criteria evaluate the authenticated
|
|
**user** certificate, not the application's channel certificate. `Application`
|
|
and `TrustedApplication` criteria continue to evaluate application identity.
|
|
|
|
Additional identity-mapping rules are evaluated by `IRoleManager.ResolveGrantedRoles` after authentication. New integrations should expose claims through `IIdentityClaims` and add `IdentityMappingRuleType` entries to the role manager.
|
|
|
|
### User Management (Part 18 §5)
|
|
|
|
The Part 18 §5 `UserManagementType` is bound to the standard
|
|
`Server.ServerConfiguration.UserManagement` object (NodeId `i=24290`).
|
|
Integrators inject an `IUserManagement` instance via
|
|
`IServerInternal.SetUserManagement` before the configuration node manager
|
|
binds the address space; the default `UserManagement` implementation wraps
|
|
an existing `IUserDatabase` for credential persistence. Built-in
|
|
`LinqUserDatabase` and `JsonUserDatabase` also persist the per-user
|
|
`UserConfigurationMask` and description:
|
|
|
|
```csharp
|
|
using Opc.Ua.Server.UserDatabase;
|
|
using Opc.Ua.Server.UserManagement;
|
|
|
|
var users = new LinqUserDatabase();
|
|
users.CreateUser("alice", "secret"u8, [Role.AuthenticatedUser]);
|
|
|
|
var userManagement = new UserManagement(
|
|
users,
|
|
passwordLength: new Range { Low = 8, High = 256 },
|
|
passwordOptions:
|
|
PasswordOptionsMask.SupportDisableUser
|
|
| PasswordOptionsMask.SupportInitialPasswordChange
|
|
| PasswordOptionsMask.RequiresDigitCharacters);
|
|
|
|
serverInternal.SetUserManagement(userManagement);
|
|
```
|
|
|
|
`IUserDatabase` commits credentials and metadata together, so disabled and
|
|
`MustChangePassword` decisions survive a restart. A custom database implements
|
|
`CreateUser`, `ResetPassword` and `UpdateUserMetadata` alongside the credential
|
|
members; each mutation must be one transaction, and a rejected or failed write
|
|
must leave the live and persisted records unchanged.
|
|
|
|
`UserManagementBinding.Bind` (called automatically by
|
|
`ConfigurationNodeManager.CreateServerConfiguration` when an
|
|
`IUserManagement` is injected) wires the typed `AddUser`, `ModifyUser`,
|
|
`RemoveUser` and `ChangePassword` method-state proxies, enforces
|
|
`RoleAuthorizationGate.CheckAdmin` on the admin methods (SecurityAdmin +
|
|
SignAndEncrypt) and `RoleAuthorizationGate.CheckSelfUserName` on
|
|
`ChangePassword`, and closes a deactivated user's sessions **and
|
|
subscriptions** through the server's coordinated teardown. `ModifyUser`
|
|
and `RemoveUser` wait for that teardown and return the user-change result.
|
|
A session-close failure is logged; it does not turn a committed user change
|
|
into a failed method result.
|
|
The binding resolves the server's session manager when needed because
|
|
the diagnostics address space is created before the session manager.
|
|
External deactivation notifications are tracked; `DisposeAsync` drains
|
|
accepted work. The master drains configuration shutdown work before acquiring
|
|
the address-space teardown gate, so pending session-close notifications can
|
|
complete without deadlocking shutdown.
|
|
|
|
Spec result codes honoured:
|
|
|
|
| Operation | Failure codes |
|
|
|-----------|---------------|
|
|
| `AddUser` (§5.2.5) | `Bad_AlreadyExists`, `Bad_OutOfRange`, `Bad_NotSupported`, `Bad_ConfigurationError`, `Bad_UserAccessDenied`, `Bad_SecurityModeInsufficient`, `Bad_ResourceUnavailable` |
|
|
| `ModifyUser` (§5.2.6) | `Bad_NotFound`, `Bad_OutOfRange`, `Bad_NotSupported`, `Bad_ConfigurationError`, `Bad_UserAccessDenied`, `Bad_SecurityModeInsufficient`, `Bad_InvalidSelfReference` |
|
|
| `RemoveUser` (§5.2.7) | `Bad_NotFound`, `Bad_UserAccessDenied`, `Bad_NotSupported` (NoDelete), `Bad_SecurityModeInsufficient`, `Bad_InvalidSelfReference` |
|
|
| `ChangePassword` (§5.2.8) | `Bad_IdentityTokenInvalid`, `Bad_OutOfRange`, `Bad_InvalidState`, `Bad_NotSupported` (NoChangeByUser), `Bad_SecurityModeInsufficient`, `Bad_AlreadyExists` (new == old) |
|
|
|
|
### User name / password storage
|
|
|
|
The `IUserDatabase` interface (`Opc.Ua.Server.UserDatabase.IUserDatabase`) remains the extension point for username/password storage. The shipped implementations are:
|
|
|
|
- `LinqUserDatabase` - in-memory.
|
|
- `JsonUserDatabase` - JSON file backed.
|
|
|
|
The JSON testing/sample database publishes a complete temporary snapshot
|
|
through atomic file replacement. Failed writes leave the previous file
|
|
intact, and saves are serialized per database instance. Missing files
|
|
initialize an empty database; malformed or inaccessible files are logged
|
|
and reported as errors instead of silently becoming empty databases.
|
|
Authentication reads committed credential and role records without waiting
|
|
for snapshot I/O; the pending replacement is visible only to snapshot
|
|
persistence until the write succeeds. A failed write never publishes its
|
|
candidate credentials.
|
|
|
|
Self-service password changes verify the captured credential and derive the
|
|
replacement outside the credential-store and metadata-reader locks, retaining
|
|
100,000-iteration PBKDF2-SHA512. The store commits only if the user's identity
|
|
and captured verifier still match, so a concurrent password reset or change
|
|
cannot be overwritten by stale verification. The password and cleared
|
|
`MustChangePassword` flag remain one atomic persisted update, preserving the
|
|
latest other metadata. Management mutations remain serialized separately from
|
|
authentication reads; self-service hashing and snapshot I/O do not hold the
|
|
metadata write lock. Administrative mutations retain their existing metadata
|
|
publication semantics.
|
|
|
|
Password verification uses the shared fixed-time comparison with the
|
|
expected derived-key width and clears working key/password buffers on
|
|
every exit. Unknown users perform bounded dummy-key verification, and
|
|
inactive users still perform credential verification before access is
|
|
denied. This equalizes the expensive verification work; it is not a claim
|
|
that all surrounding application behavior has constant execution time.
|
|
|
|
External implementations may be provided by integrators (e.g. backed by SQL, LDAP, or a vendor-specific store).
|
|
|
|
## Client side
|
|
|
|
`Opc.Ua.Client.Roles.RoleManagementClient` is a strongly-typed async client over the standard `Server.ServerCapabilities.RoleSet` object:
|
|
|
|
```csharp
|
|
using Opc.Ua.Client.Roles;
|
|
|
|
RoleManagementClient client = session.OpenRoleManagementClient();
|
|
|
|
// List every role exposed by the server.
|
|
IReadOnlyList<RoleInfo> roles = await client.ListRolesAsync();
|
|
|
|
// Add a UserName identity rule to the Observer role.
|
|
await client.AddIdentityAsync(
|
|
Opc.Ua.ObjectIds.WellKnownRole_Observer,
|
|
new IdentityMappingRuleType
|
|
{
|
|
CriteriaType = IdentityCriteriaType.UserName,
|
|
Criteria = "alice"
|
|
});
|
|
|
|
// Toggle the ApplicationsExclude flag on the Operator role.
|
|
await client.SetApplicationsExcludeAsync(
|
|
Opc.Ua.ObjectIds.WellKnownRole_Operator, value: false);
|
|
|
|
// Add a custom role under a vendor namespace.
|
|
NodeId newRoleId = await client.AddRoleAsync(
|
|
"OpsLead",
|
|
namespaceUri: "urn:example:roles");
|
|
|
|
// Remove a dynamic role.
|
|
await client.RemoveRoleAsync(newRoleId);
|
|
```
|
|
|
|
`ListRolesAsync` includes both `RoleType` instances and instances of its
|
|
subtypes, resolving derived types through the session's node cache. Other
|
|
object types under the RoleSet are excluded.
|
|
|
|
All mutator methods require the calling session to hold the `SecurityAdmin` role and use a `SignAndEncrypt` secure channel; otherwise the server returns `Bad_UserAccessDenied` / `Bad_SecurityModeInsufficient` which surfaces as a `ServiceResultException`.
|
|
|
|
### Bounded role snapshots
|
|
|
|
Use `new RoleManagementClient(session, maxMappingEntries: 1000)` to limit a role
|
|
snapshot. The limit applies separately to identity, application and endpoint
|
|
mapping collections, before client-side decoded-array/list copies. Zero and the
|
|
existing one-argument constructor preserve legacy behavior. This does not bound
|
|
the separate `ListRolesAsync` traversal.
|
|
|
|
When a target check and subsequent role calls must use the same server session,
|
|
capture a [`SessionRequestContext`](Sessions.md#generation-bound-client-requests)
|
|
first. Resolve the target through `context.Session` and use
|
|
`new RoleManagementClient(context.Session, maxMappingEntries: 1000)` until the
|
|
operation is finished. Recovery then refuses later calls instead of silently
|
|
moving the operation to a replacement session.
|
|
|
|
## GDS
|
|
|
|
The GDS adds the following roles in addition to the standard well-known set:
|
|
|
|
- `DiscoveryAdmin`
|
|
- `SecurityAdmin`
|
|
- `CertificateAuthorityAdmin`
|
|
- `RegistrationAuthorityAdmin`
|
|
- `ApplicationSelfAdmin`
|
|
|
|
See [GdsRole.cs](https://github.com/OPCFoundation/UA-.NETStandard/blob/main/src/Opc.Ua.Gds.Server/RoleBasedUserManagement/GdsRole.cs).
|
|
|
|
## Known limitations
|
|
|
|
- **Integration tests for end-to-end RoleSet / UserManagement scenarios** (e.g. AddRole over the wire followed by browse + AddIdentity + activated session under the new grant) are tracked as a follow-up.
|
|
|
|
## References
|
|
|
|
- [OPC UA Part 18 v1.05.06 - Role-Based Security](https://reference.opcfoundation.org/Core/Part18/v105/docs/)
|
|
- [OPC UA Part 3 4.9 - Well-Known Roles](https://reference.opcfoundation.org/Core/Part3/v105/docs/4.9)
|
|
- `src/Opc.Ua.Server/RoleBasedUserManagement/`
|
|
- `src/Opc.Ua.Client/Roles/`
|
|
- `tests/Opc.Ua.Server.Tests/RoleManagerTests.cs`
|