# Role-Based Security (OPC UA Part 18) ## 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. > **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.Bind(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. ### 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. 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 and stores the per-user `UserConfigurationMask` and description in memory: ```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); ``` `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 any active sessions for a deactivated user via the supplied `ISessionManager`. 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. 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 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); ``` 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`. ## 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.Common/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`