mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-29 07:07:24 +08:00
# Description Introduces a pluggable user-identity provider model covering all OPC UA Part 6 §6.5 mechanisms (Anonymous, UserName, X509, IssuedToken/JWT) on both client and server, with full `Microsoft.Extensions.DependencyInjection` integration, plus the Part 12 §9 `AuthorizationService` two-phase token-request flow **including refresh-token rotation**, the Part 12 §8 KeyCredential Push model, and a clean provider model for OPC 10000-12 §7.2 `ApplicationSelfAdmin`. The work is split across phase-by-phase commits so it can be reviewed independently; nothing here changes wire-level behaviour or forces existing applications to migrate. ### Documentation All under `Docs/`: - `IdentityProviders.md` — full developer guide: three-layer model, recipes, configuration reference, "what gets auto-wired" matrix, JWS algorithm table, identity-augmenters section, **"Implementing your own provider"** how-to subsections for Entra / OIDC / Windows / AspNetCore (sibling files merged in per review feedback). - `DependencyInjection.md` — `Identity` sections with `appsettings.json` examples, identity-extension matrix, `WithAuthorizationService` + `WithKeyCredentialPush` + `AddIdentityAugmenter` extensions documented. - `RoleBasedUserManagement.md` — claim-criteria callout describing `IIdentityClaims` probe surface. - `AuthorizationService.md` — Architecture diagram + `IAccessTokenProvider` conceptual narrative restored from master; modern `StartRequestToken` / `FinishRequestToken` flow leads; refresh-token recipe added. - `KeyCredentialService.md` — Push model + Pull vs Push vs Bridge decision matrix + Hybrid (Push + Pull) section; explicit experimental warning for the bridge. - `GDS.md` — KeyCredential Push, `StartRequestToken`, `GdsApplicationSelfAdminProvider`, **shipped Refresh-token support with rotation policy and audit semantics** all documented. - `MigrationGuide.md` — User Identity Providers section listing `[Obsolete]` markers + mechanical recipes; SelfAdmin migration via `IIdentityAugmenter`; `EnableRefreshTokens` default note. - `README.md` index entries. ## Checklist - [x] 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. - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [x] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed **all** PR feedback received.
182 lines
7.6 KiB
C#
182 lines
7.6 KiB
C#
using System.Threading.Tasks;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Server.TestFramework;
|
|
|
|
namespace Opc.Ua.Server.Tests
|
|
{
|
|
[TestFixture]
|
|
[Category("Event")]
|
|
public class SessionTests
|
|
{
|
|
[Test]
|
|
public async Task UpdateDiagnosticCounters_RaisesEvent_WhenPerRequestCounterChangedAsync()
|
|
{
|
|
var fixture = new ServerFixture<StandardServer>(t => new StandardServer(t));
|
|
await fixture.StartAsync().ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
StandardServer server = fixture.Server;
|
|
|
|
(RequestHeader requestHeader, SecureChannelContext secureChannelContext) =
|
|
await server.CreateAndActivateSessionAsync("UpdateDiagnosticCountersTest").ConfigureAwait(false);
|
|
|
|
ISession session = server.CurrentInstance.SessionManager.GetSession(requestHeader.AuthenticationToken);
|
|
Assert.That(session, Is.Not.Null, "Session should exist after Create/Activate.");
|
|
|
|
bool eventRaised = false;
|
|
|
|
server.CurrentInstance.SessionManager.SessionDiagnosticsChanged += (s, reason)
|
|
=> eventRaised = true;
|
|
|
|
uint before = session.SessionDiagnostics.ReadCount.TotalCount;
|
|
|
|
// Call ValidateRequest for a request type that maps to a counter (Read).
|
|
session.ValidateRequest(requestHeader, secureChannelContext, RequestType.Read);
|
|
|
|
Assert.That(eventRaised, Is.True, "SessionDiagnosticsChanged event should be raised when a per-request counter changes.");
|
|
Assert.That(session.SessionDiagnostics.ReadCount.TotalCount, Is.GreaterThan(before), "ReadCount.TotalCount should have incremented.");
|
|
}
|
|
finally
|
|
{
|
|
await fixture.StopAsync().ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
[Category("Event")]
|
|
[TestCase(RequestType.Unknown)]
|
|
[TestCase(RequestType.FindServers)]
|
|
[TestCase(RequestType.GetEndpoints)]
|
|
[TestCase(RequestType.CreateSession)]
|
|
[TestCase(RequestType.ActivateSession)]
|
|
[TestCase(RequestType.CloseSession)]
|
|
[TestCase(RequestType.Cancel)]
|
|
public async Task UpdateDiagnosticCounters_DoesNotRaiseEvent_ForIgnoredRequestTypesAsync(RequestType requestType)
|
|
{
|
|
var fixture = new ServerFixture<StandardServer>(t => new StandardServer(t));
|
|
await fixture.StartAsync().ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
StandardServer server = fixture.Server;
|
|
|
|
(RequestHeader requestHeader, SecureChannelContext secureChannelContext) =
|
|
await server.CreateAndActivateSessionAsync("UpdateDiagnosticCountersIgnoredTest").ConfigureAwait(false);
|
|
|
|
ISession session = server.CurrentInstance.SessionManager.GetSession(requestHeader.AuthenticationToken);
|
|
Assert.That(session, Is.Not.Null, "Session should exist after Create/Activate.");
|
|
|
|
bool eventRaised = false;
|
|
|
|
server.CurrentInstance.SessionManager.SessionDiagnosticsChanged += (s, reason)
|
|
=> eventRaised = true;
|
|
|
|
// Capture total requests before; UpdateDiagnosticCounters always increments TotalRequestCount.
|
|
uint totalBefore = session.SessionDiagnostics.TotalRequestCount.TotalCount;
|
|
|
|
// Call ValidateRequest with one of the ignored request types.
|
|
session.ValidateRequest(requestHeader, secureChannelContext, requestType);
|
|
|
|
Assert.That(
|
|
session.SessionDiagnostics.TotalRequestCount.TotalCount,
|
|
Is.EqualTo(totalBefore + 1),
|
|
"TotalRequestCount should increment for all request types.");
|
|
|
|
Assert.That(eventRaised, Is.False, $"SessionDiagnosticsChanged event must NOT be raised for request type {requestType}.");
|
|
}
|
|
finally
|
|
{
|
|
await fixture.StopAsync().ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------
|
|
// Part 18 §4.4.1 — live identity re-evaluation contract
|
|
// ------------------------------------------------------------
|
|
|
|
[Test]
|
|
public async Task IsIdentityStale_FreshSession_StartsFalseAsync()
|
|
{
|
|
var fixture = new ServerFixture<StandardServer>(t => new StandardServer(t));
|
|
await fixture.StartAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
StandardServer server = fixture.Server;
|
|
(RequestHeader requestHeader, _) =
|
|
await server.CreateAndActivateSessionAsync("StaleFresh").ConfigureAwait(false);
|
|
|
|
ISession session = server.CurrentInstance.SessionManager
|
|
.GetSession(requestHeader.AuthenticationToken);
|
|
|
|
Assert.That(session.IsIdentityStale, Is.False,
|
|
"Newly activated sessions must not be marked stale.");
|
|
}
|
|
finally
|
|
{
|
|
await fixture.StopAsync().ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task MarkIdentityStale_SetsFlag_AndRefreshClearsItAsync()
|
|
{
|
|
var fixture = new ServerFixture<StandardServer>(t => new StandardServer(t));
|
|
await fixture.StartAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
StandardServer server = fixture.Server;
|
|
(RequestHeader requestHeader, _) =
|
|
await server.CreateAndActivateSessionAsync("StaleFlag").ConfigureAwait(false);
|
|
|
|
ISession session = server.CurrentInstance.SessionManager
|
|
.GetSession(requestHeader.AuthenticationToken);
|
|
|
|
IUserIdentity original = session.EffectiveIdentity;
|
|
Assert.That(original, Is.Not.Null);
|
|
|
|
session.MarkIdentityStale();
|
|
Assert.That(session.IsIdentityStale, Is.True);
|
|
|
|
// Calling MarkIdentityStale again is idempotent.
|
|
session.MarkIdentityStale();
|
|
Assert.That(session.IsIdentityStale, Is.True);
|
|
|
|
session.RefreshEffectiveIdentity(original);
|
|
Assert.That(session.IsIdentityStale, Is.False,
|
|
"RefreshEffectiveIdentity must clear the stale flag.");
|
|
Assert.That(session.EffectiveIdentity, Is.SameAs(original),
|
|
"Refresh must replace the EffectiveIdentity atomically.");
|
|
}
|
|
finally
|
|
{
|
|
await fixture.StopAsync().ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task RefreshEffectiveIdentity_NullIdentity_ThrowsAsync()
|
|
{
|
|
var fixture = new ServerFixture<StandardServer>(t => new StandardServer(t));
|
|
await fixture.StartAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
StandardServer server = fixture.Server;
|
|
(RequestHeader requestHeader, _) =
|
|
await server.CreateAndActivateSessionAsync("StaleArg").ConfigureAwait(false);
|
|
|
|
ISession session = server.CurrentInstance.SessionManager
|
|
.GetSession(requestHeader.AuthenticationToken);
|
|
|
|
Assert.Throws<System.ArgumentNullException>(
|
|
() => session.RefreshEffectiveIdentity(null!),
|
|
"Passing a null identity must throw to prevent corrupt session state.");
|
|
}
|
|
finally
|
|
{
|
|
await fixture.StopAsync().ConfigureAwait(false);
|
|
}
|
|
}
|
|
}
|
|
}
|