Files
Marc Schier 2966f693b2 2.0 Migration Analyzer and codefixer with Agent update (#3820)
Introduces `OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer` — a
single NuGet package that ships **three Roslyn components + a runtime
shim** to make the 1.5.378 → 2.0 upgrade as close to "install package,
run `dotnet format`, remove package" as possible:

| DLL | Where | Purpose |
|-----|-------|---------|
| `Opc.Ua.MigrationAnalyzer.dll` | `analyzers/dotnet/cs/` | 19 Roslyn
analyzers (`UA0001`–`UA0022`) covering every breaking-change pattern in
[`Docs/MigrationGuide.md`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md).
Built against the stable Roslyn 4.14 analyzer API — loads cleanly in
csc.exe (no `Microsoft.CodeAnalysis.Workspaces` reference). |
| `Opc.Ua.MigrationAnalyzer.CodeFixer.dll` | `analyzers/dotnet/cs/` | 14
companion `CodeFixProvider`s; loaded only by Workspaces-aware hosts
(Visual Studio, `dotnet format analyzers`). |
| `Opc.Ua.MigrationAnalyzer.Generator.dll` | `analyzers/dotnet/cs/` |
Roslyn `IIncrementalGenerator` that emits per-consumer `internal sealed
[Obsolete] class <Name>Collection : List<TElement>` shims for every
`<Type>Collection` wrapper the consumer references — including
**arbitrary model-compiled `<UserType>Collection` patterns**, not just
the 30 well-known built-in ones. |
| `Opc.Ua.MigrationAnalyzer.Core.dll` | `lib/<tfm>/` for `net472, net48,
netstandard2.1, net8.0, net9.0, net10.0` | 1.5.378 → 2.0 runtime
compatibility shim. Re-exposes the obsolete extension surface so
1.5.378-style call sites keep compiling against 2.0 with warnings
instead of errors. |

### Migration workflow the package enables

1. **Bump every `OPCFoundation.NetStandard.Opc.Ua.*` PackageReference to
`2.0.*-*`.**
2. **Add the migration package** as a build-only dependency:
   ```xml
<PackageReference
Include="OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer"
                     Version="2.0.*-*"
                     PrivateAssets="all" />
   ```
3. **Run `dotnet build`.** The Core shim DLL re-supplies obsolete
extension members; the source generator covers `<Type>Collection`
references; what remains are `[Obsolete]` warnings + `UA00xx` analyzer
diagnostics + (occasionally) `MIG01` for unresolvable model-compiled
element types.
4. **Apply auto-fixes** in the IDE or from the command line:
   ```bash
   dotnet format analyzers <solution>.sln \
       --diagnostics UA0002 UA0003 UA0004 UA0005 UA0006 UA0007 UA0008 \
                     UA0009 UA0010 UA0012 UA0014 UA0019 UA0020 UA0022 \
       --severity warn
   ```
5. **Walk the residual** `UA0001` / `UA0011` / `UA0015` / `UA0018` /
`UA0021` diagnostics manually — they cover patterns that require human
judgement.
6. **Remove the migration package.** You are now on clean 2.0 with no
shim dependency.

### Source generator (`Opc.Ua.MigrationAnalyzer.Generator.dll`)

Per-consumer Roslyn `IIncrementalGenerator` that emits `.g.cs` files for
every `<Name>Collection` reference in the consumer's compilation that
fails to bind:

- **Catalog override (30 entries)** — pins element types that renamed
across the 1.5.378 → 2.0 boundary: `DateTime`→`DateTimeUtc`,
`Guid`→`Uuid`, `byte[]`→`ByteString`, `XmlElement`→`Opc.Ua.XmlElement`,
plus the primitives and built-in opc types whose element name didn't
rename.
- **Semantic lookup** — for model-compiled `<UserType>Collection`
patterns the model compiler historically emitted for any user-defined
complex type. Strips the `Collection` suffix, runs
`Compilation.GetSymbolsWithName` against the unique element short name,
emits the shim with the fully-qualified type reference.
- **`MIG01` diagnostic** — fires when the element type can't be uniquely
resolved (zero or multiple candidates). User adds the missing `using` or
migrates the site manually.

Each generated file emits a class shaped exactly like the 1.5.378
wrapper:

```csharp
[global::System.Obsolete("'Int32Collection' was removed in 2.0. Use 'List<int>' or 'ArrayOf<int>' instead. (UA0002)")]
internal sealed class Int32Collection : global::System.Collections.Generic.List<int>
{
    public Int32Collection() { }
    public Int32Collection(int capacity) : base(capacity) { }
    public Int32Collection(global::System.Collections.Generic.IEnumerable<int> collection) : base(collection) { }
    public static implicit operator global::Opc.Ua.ArrayOf<int>(Int32Collection? value)
        => value is null ? default : value.ToArrayOf();
}
```

**Design choice — `internal` (not `public`):** the generated shim never
leaks through the consumer's public API surface. Consumers with *public*
methods or properties returning a `<Type>Collection` will hit `CS0050:
Inconsistent accessibility` — the intended signal that the public
surface must migrate to `List<T>` / `ArrayOf<T>` first; internal call
sites keep compiling under the shim so iteration is incremental.

### 19-rule analyzer reference

| Rule | Replaces | Default | Auto-fix |
|------|----------|---------|----------|
| `UA0001` | `Utils.Trace` / `Utils.LogX` | Info | No (telemetry
plumbing requires manual review) |
| `UA0002` | Removed `<Type>Collection` wrappers | Warning | Yes (→
`List<T>` / `ArrayOf<T>`); the source generator keeps the call site
compiling in the meantime |
| `UA0003` | `x == null` on now-struct built-ins | Warning | Yes (→
`x.IsNull`) |
| `UA0004` | `?.` on now-struct built-ins | Warning | Yes (drop the `?`)
|
| `UA0005` | `byte[]` where `ByteString` is expected | Warning | Yes
(append `.ToByteString()`) |
| `UA0006` | `new Variant(object\|DateTime\|Guid\|byte[])` | Warning |
Yes (→ `Variant.From(...)`) |
| `UA0007` | `new NodeId(string)` / `new ExpandedNodeId(string)` |
Warning | Yes (→ `Parse`) |
| `UA0008` | `Session.Call(..., params object[])` | Warning | Yes (wrap
with `Variant.From`) |
| `UA0009` | `[DataContract]` / `[DataMember]` on config extensions |
Warning | Yes (→ `[DataType]` / `[DataTypeField]`) |
| `UA0010` | `using` / `Dispose` on `CertificateIdentifier` /
`UserIdentity` / `IUserIdentityTokenHandler` | Warning | Yes (drop the
disposable) |
| `UA0011` | Sync
`IUserIdentityTokenHandler.{Encrypt,Decrypt,Sign,Verify}` | Info | No
(async promotion required) |
| `UA0012` | `CertificateFactory.*` static helpers | Warning | Yes (→
instance methods) |
| `UA0014` | `DataValue.IsGood(dv)` static helper | Warning | Yes (→
`dv.IsGood`) |
| `UA0015` | Sync / APM members on GDS / LDS clients | Info | No (async
promotion required) |
| `UA0018` | `CertificateIdentifier.Certificate` getter | Info | No (→
`LoadCertificate2Async`) |
| `UA0019` | `new DataValue(StatusCode[, ts])` | Warning | Yes (→
object-initializer form) |
| `UA0020` | `EncodeableFactory.GlobalFactory` / `Create()` | Warning |
Yes (→ `ServiceMessageContext.Factory` / `Fork()`) |
| `UA0021` | `CertificateValidator` / `CertificateValidationEventArgs`
(structural rename) | Info | No (see migration guide §Certificate
validation) |
| `UA0022` | `ApplicationConfiguration.CertificateValidator` /
`ServerBase.CertificateValidator` property rename | Warning | Yes (→
`.CertificateManager`) |

Plus one generator-only diagnostic:

| Rule | Source | Default | Triggers |
|------|--------|---------|----------|
| `MIG01` | Generator | Warning | Generator can't uniquely resolve the
element type for a `<Foo>Collection` reference (zero or multiple
candidates). The user adds a `using` for the namespace defining `Foo`,
or migrates the call site manually. |

### Runtime shim coverage (`Opc.Ua.MigrationAnalyzer.Core.dll`)

C# 14 `extension` members so 1.5.378 call sites continue to compile:

- **Moved obsolete extensions** the 2.0 libraries no longer carry
inline: `NodeId` / `Variant` / `DataValue` null-check helpers, `Session`
sync helpers, `Subscription` sync helpers, `ApplicationInstance`
helpers, `ServerBase.Start` / `Stop`, `TransportChannel` APM,
`ChannelBase` static factory methods, etc.
- **New shims for genuinely-removed members**:
`EncodeableFactory.GlobalFactory`, `CertificateIdentifier.Certificate`
(throws), sync wrappers for
`IUserIdentityTokenHandler.{Encrypt,Decrypt,Sign,Verify}`, sync + APM
wrappers for the GDS / LDS client APIs, `GlobalDiscoverySampleServer`
1.5.378-shape ctor (in-tree `Libraries/Opc.Ua.Gds.Server.Common/`).

> ⚠ The sync shims wrap their `*Async` counterparts via
`Task.Run(…).GetAwaiter().GetResult()`. Intended as a **migration aid
only** — port to `await` before shipping.

### Repo layout

```
Tools/Opc.Ua.MigrationAnalyzer/                  # analyzer DLL
  Analyzers/UA00xxAnalyzer.cs                    # 19 DiagnosticAnalyzer impls
  Diagnostics/{DiagnosticIds,Descriptors,WellKnownProperties}.cs
  Helpers/{SymbolExtensions,UaSymbols}.cs
  Opc.Ua.MigrationAnalyzer.nuspec                # ships all 3 analyzer DLLs + 6-TFM Core
  OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props

Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/        # code-fix DLL
  CodeFixes/UA00xxCodeFix.cs                     # 14 CodeFixProvider impls

Tools/Opc.Ua.MigrationAnalyzer.Generator/        # source generator DLL  (NEW)
  MigrationGenerator.cs                          # IIncrementalGenerator
  CollectionShimCatalog.cs                       # 30-entry element-type overrides
  Diagnostics/GeneratorDiagnostics.cs            # MIG01

Tools/Opc.Ua.MigrationAnalyzer.Core/             # runtime shim (multi-TFM)
  Client/, Configuration/, Core/, Gds.Client.Common/, Types/, Marker/

Tools/Roslyn.slnx                                # source-gen + migration projects

Tests/Opc.Ua.MigrationAnalyzer.Tests/            # 142 tests
  Analyzers/UA00xxTests.cs                       # 107 analyzer + code-fix tests
  Generators/MigrationGeneratorTests.cs          # 35 generator tests (NEW)
Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/       # 13 runtime-shim tests
```

### Documentation

-
[`Docs/MigrationGuide.md`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md)
— §3 Collection Type Migration gains a tip pointing at the generator so
CS0246 is replaced with `[Obsolete]` warnings + UA0002.
-
[`.github/agents/opcua-v20-migration.agent.md`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/.github/agents/opcua-v20-migration.agent.md)
— Strategy section mentions the three Roslyn components + runtime shim
explicitly, plus the MIG01 diagnostic and the "internal-by-design"
CS0050 hint.

### Central package pinning

`Directory.Packages.props` pins `Microsoft.CodeAnalysis.*` (Common,
CSharp, CSharp.Workspaces, Workspaces.Common) to **4.14.0** — the stable
analyzer API surface. The earlier 5.3 pin was the *csc-internal*
version, which is not safe for analyzer hosts.

### Dogfood story

The full 42-sample dogfood against
`OPCFoundation/UA-.NETStandard-Samples` surfaced a **critical
analyzer-loader bug** in an earlier iteration: the analyzer DLL
initially co-shipped its code-fix providers in one assembly, which
transitively referenced `Microsoft.CodeAnalysis.Workspaces.dll`.
csc.exe's analyzer host ships only `Microsoft.CodeAnalysis.dll` +
`CSharp.dll` in its bincore and **silently swallows** load failures —
net result: zero diagnostics across all samples. Fix landed in commit
`861fa6ee1`:

1. **Split** the assembly: analyzer in `Opc.Ua.MigrationAnalyzer.dll`
(Workspaces-free, csc-safe), code-fixes in
`Opc.Ua.MigrationAnalyzer.CodeFixer.dll` (Workspaces-aware, IDE-only).
2. **Downgrade** to Roslyn 4.14 — the stable analyzer API.
3. `/p:ReportAnalyzer=true` confirms all 19 analyzers initialise and
execute on real consumer code.

This PR's new generator was smoke-tested against a minimal consumer that
exercises all 30 catalog entries + a model-compiled element type + the
unresolvable case:
- 30 catalog patterns + model-compiled `MyThingCollection` → all emit
shims, fire CS0618 with `(UA0002)` marker
- Element-type renames verified: `DateTimeCollection` → `DateTimeUtc`,
`ByteStringCollection` → `ByteString`, `GuidCollection` → `Uuid`, etc.
- `NeverHeardOfItCollection` → `MIG01` fires with help link to the
migration guide

Known compatibility gaps (documented in the migration agent):
- Legacy WinForms `.Net4` sample projects use pre-SDK MSBuild XML and
**ignore** `Directory.Build.targets` `<PackageReference>` injection —
consumers in this format must add the migration package to their csproj
inline.

### Packaging design notes

- Hand-authored nuspec ships 3 DLLs in `analyzers/dotnet/cs/` + 6 TFMs
of Core (shim) DLLs under `lib/`. All 6 OPC UA runtime package
dependencies are declared per-TFM-group.
- `build/OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props` is
auto-imported by NuGet (file name must match package id exactly) and
injects all three analyzer DLLs as `<Analyzer>` items.
- `DiagnosticIds.cs` / `DiagnosticDescriptors.cs` /
`WellKnownProperties.cs` / `Helpers/*.cs` are **shared via `<Compile
Link=…>`** between the analyzer and code-fixer projects to break what
would otherwise be a NuGet restore cycle. The generator has no compile
dependency on the analyzer; the 30-entry override table is duplicated in
`CollectionShimCatalog.cs` (the analyzer table also drives diagnostic
messages, so the duplication is small and intentional).
- `Opc.Ua.MigrationAnalyzer.Core` is multi-TFM (`net472, net48,
netstandard2.1, net8.0, net9.0, net10.0`) and **non-packable on its
own**; the parent migration package's nuspec consumes each TFM's DLL by
path.
2026-06-05 12:01:53 +02:00

488 lines
20 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Opc.Ua.Gds.Server.Database;
using Opc.Ua.Gds.Server.Identity;
using Opc.Ua.Identity;
using Opc.Ua.Security.Certificates;
using Opc.Ua.Server;
using Opc.Ua.Server.UserDatabase;
namespace Opc.Ua.Gds.Server
{
/// <summary>
/// Implements a sample Global Discovery Server.
/// </summary>
/// <remarks>
/// <para>
/// Each server instance must have one instance of a StandardServer object which is
/// responsible for reading the configuration file, creating the endpoints and dispatching
/// incoming requests to the appropriate handler.
/// </para>
/// <para>
/// This sub-class specifies non-configurable metadata such as Product Name and initializes
/// the ApplicationNodeManager which provides access to the data exposed by the
/// Global Discovery Server.
/// </para>
/// </remarks>
public class GlobalDiscoverySampleServer : StandardServer
{
public GlobalDiscoverySampleServer(
IApplicationsDatabase database,
ICertificateRequest request,
ICertificateGroup certificateGroup,
IUserDatabase userDatabase,
ITelemetryContext telemetry,
bool autoApprove = true)
: this(
database,
request,
certificateGroup,
userDatabase,
telemetry,
autoApprove,
enableApplicationSelfAdminProvider: true)
{
}
public GlobalDiscoverySampleServer(
IApplicationsDatabase database,
ICertificateRequest request,
ICertificateGroup certificateGroup,
IUserDatabase userDatabase,
ITelemetryContext telemetry,
bool autoApprove,
bool enableApplicationSelfAdminProvider)
: base(telemetry)
{
m_database = database;
m_request = request;
m_certificateGroup = certificateGroup;
m_userDatabase = userDatabase;
m_autoApprove = autoApprove;
m_enableApplicationSelfAdminProvider = enableApplicationSelfAdminProvider;
}
/// <summary>
/// Back-compat ctor matching the 1.5.378 signature (no <see cref="ITelemetryContext"/>).
/// Forwards to the modern ctor with a null telemetry context.
/// </summary>
/// <remarks>
/// Preserved so 1.5.378-style sample code (`new GlobalDiscoverySampleServer(database,
/// request, certificateGroup, userDatabase, autoApprove)`) continues to compile against
/// 2.0 without re-ordering the call site. Consumers should pass an explicit
/// <see cref="ITelemetryContext"/> via the non-obsolete ctor.
/// </remarks>
[Obsolete("Use the constructor that takes an ITelemetryContext parameter instead.")]
public GlobalDiscoverySampleServer(
IApplicationsDatabase database,
ICertificateRequest request,
ICertificateGroup certificateGroup,
IUserDatabase userDatabase,
bool autoApprove = true)
: this(database, request, certificateGroup, userDatabase, telemetry: null!, autoApprove)
{
}
/// <summary>
/// Called before the server starts. Registers GDS-specific
/// encodeable types in the server's message context factory,
/// which is required for NativeAOT where reflection-based
/// assembly scanning does not discover types automatically.
/// </summary>
protected override void OnServerStarting(
ApplicationConfiguration configuration)
{
base.OnServerStarting(configuration);
if (!MessageContext.Factory.ContainsEncodeableType(
DataTypeIds.ApplicationRecordDataType))
{
MessageContext.Factory.Builder
.AddOpcUaGds()
.AddOpcUaGdsServerDataTypes()
.Commit();
}
}
/// <summary>
/// Called after the server has been started.
/// </summary>
protected override void OnServerStarted(IServerInternal server)
{
base.OnServerStarted(server);
server.IdentityRegistry.Register(new AnonymousAuthenticator());
server.IdentityRegistry.Register(new GlobalDiscoverySampleUserNameAuthenticator(this));
server.IdentityRegistry.Register(new GlobalDiscoverySampleX509Authenticator(this));
if (m_enableApplicationSelfAdminProvider)
{
server.IdentityRegistry.RegisterAugmenter(
new GdsApplicationSelfAdminProvider(
m_database,
server.Telemetry.CreateLogger<GdsApplicationSelfAdminProvider>(),
server.NamespaceUris,
IsApplicationCertificateRegistered));
}
}
/// <summary>
/// Creates the node managers for the server.
/// </summary>
/// <remarks>
/// This method allows the sub-class create any additional node managers which it uses.
/// The SDK always creates a CoreNodeManager which handles the built-in nodes defined
/// by the specification.
/// Any additional NodeManagers are expected to handle application specific nodes.
/// </remarks>
protected override ValueTask<IMasterNodeManager> CreateMasterNodeManagerAsync(
IServerInternal server,
ApplicationConfiguration configuration,
CancellationToken cancellationToken = default)
{
m_logger.LogInformation("Creating the Node Managers.");
// create the custom node managers.
var nodeManagers = new IAsyncNodeManager[]
{
new ApplicationsNodeManager(
server,
configuration,
m_database,
m_request,
m_certificateGroup,
m_autoApprove)
};
// create master node manager.
#pragma warning disable CA2000 // ownership of MasterNodeManager transfers to the caller via the returned ValueTask<IMasterNodeManager>
return new ValueTask<IMasterNodeManager>(
new MasterNodeManager(server, configuration, null, nodeManagers));
#pragma warning restore CA2000
}
/// <summary>
/// Loads the non-configurable properties for the application.
/// </summary>
/// <remarks>
/// These properties are exposed by the server but cannot be changed by administrators.
/// </remarks>
protected override ServerProperties LoadServerProperties()
{
return new ServerProperties
{
ManufacturerName = "Some Company Inc",
ProductName = "Global Discovery Server",
ProductUri = "http://somecompany.com/GlobalDiscoveryServer",
SoftwareVersion = Utils.GetAssemblySoftwareVersion(),
BuildNumber = Utils.GetAssemblyBuildNumber(),
BuildDate = Utils.GetAssemblyTimestamp()
};
}
/// <summary>
/// This method is called at the being of the thread that processes a request.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
protected override async ValueTask<OperationContext> ValidateRequestAsync(
SecureChannelContext secureChannelContext,
[NotNull] RequestHeader? requestHeader,
RequestType requestType,
RequestLifetime requestLifetime)
{
OperationContext context = await base.ValidateRequestAsync(
secureChannelContext,
requestHeader,
requestType,
requestLifetime).ConfigureAwait(false);
if (requestType == RequestType.Write)
{
// reject all writes if no user provided.
if (context.UserIdentity.TokenType == UserTokenType.Anonymous)
{
// construct translation object with default text.
var info = new TranslationInfo(
"NoWriteAllowed",
"en-US",
"Must provide a valid user before calling write.");
// create an exception with a vendor defined sub-code.
throw new ServiceResultException(
new ServiceResult(
Namespaces.OpcUaGds,
new StatusCode(StatusCodes.BadUserAccessDenied.Code, "NoWriteAllowed"),
new LocalizedText(info)));
}
}
return context;
}
private bool IsApplicationCertificateRegistered(Certificate applicationInstanceCertificate)
{
if (applicationInstanceCertificate == null)
{
throw new ArgumentNullException(nameof(applicationInstanceCertificate));
}
GlobalDiscoveryServerConfiguration configuration =
Configuration!.ParseExtension<GlobalDiscoveryServerConfiguration>()
?? new GlobalDiscoveryServerConfiguration();
var certificateStoreIdentifier = new CertificateStoreIdentifier(
configuration.ApplicationCertificatesStorePath!);
using (ICertificateStore applicationsStore =
certificateStoreIdentifier.OpenStore(MessageContext.Telemetry))
{
using CertificateCollection matchingCerts = applicationsStore
.FindByThumbprintAsync(applicationInstanceCertificate.Thumbprint)
.GetAwaiter()
.GetResult();
if (matchingCerts.Count == 0)
{
return false;
}
}
certificateStoreIdentifier = new CertificateStoreIdentifier(
configuration.AuthoritiesStorePath!);
using ICertificateStore authoritiesStore =
certificateStoreIdentifier.OpenStore(MessageContext.Telemetry);
IEnumerable<X509CRL> certificateRevocationLists = authoritiesStore
.EnumerateCRLsAsync()
.GetAwaiter()
.GetResult();
foreach (X509CRL crl in certificateRevocationLists)
{
if (crl.IsRevoked(applicationInstanceCertificate))
{
return false;
}
}
return true;
}
/// <summary>
/// Verifies that a certificate user token is trusted.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
private void VerifyX509IdentityToken(X509IdentityToken token)
{
using Certificate? userCertificate = token.CertificateData.IsEmpty
? null
: Certificate.FromRawData(token.CertificateData);
try
{
// Validate against the Users trust list using the new
// CertificateManager pipeline. Throws on validation failure.
// CA2025: task awaited via GetAwaiter().GetResult(); the disposable's
// using scope extends past the await.
#pragma warning disable CA2025
CertificateValidationResult result = CertificateManager!
.ValidateAsync(
userCertificate!,
TrustListIdentifier.Users)
.GetAwaiter().GetResult();
#pragma warning restore CA2025
if (!result.IsValid)
{
throw new ServiceResultException(result.StatusCode);
}
}
catch (Exception e)
{
TranslationInfo info;
StatusCode result = StatusCodes.BadIdentityTokenRejected;
if (e is ServiceResultException se &&
se.StatusCode == StatusCodes.BadCertificateUseNotAllowed)
{
info = new TranslationInfo(
"InvalidCertificate",
"en-US",
"'{0}' is an invalid user certificate.",
userCertificate?.Subject ?? string.Empty);
result = StatusCodes.BadIdentityTokenInvalid;
}
else
{
// construct translation object with default text.
info = new TranslationInfo(
"UntrustedCertificate",
"en-US",
"'{0}' is not a trusted user certificate.",
userCertificate?.Subject ?? string.Empty);
}
// create an exception with a vendor defined sub-code.
throw new ServiceResultException(
new ServiceResult(
LoadServerProperties().ProductUri,
new StatusCode(result.Code, info.Key),
new LocalizedText(info)));
}
}
private bool VerifyPassword(UserNameIdentityTokenHandler userTokenHandler)
{
return m_userDatabase.CheckCredentials(
userTokenHandler.UserName,
userTokenHandler.DecryptedPassword);
}
private sealed class GlobalDiscoverySampleUserNameAuthenticator : IUserTokenAuthenticator
{
private readonly GlobalDiscoverySampleServer m_server;
public GlobalDiscoverySampleUserNameAuthenticator(GlobalDiscoverySampleServer server)
{
m_server = server ?? throw new ArgumentNullException(nameof(server));
}
public UserTokenType TokenType => UserTokenType.UserName;
public string? IssuedTokenProfileUri => null;
public ValueTask<AuthenticationResult> AuthenticateAsync(
AuthenticationContext context,
CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
if (context.TokenHandler is UserNameIdentityTokenHandler userNameToken)
{
return new ValueTask<AuthenticationResult>(AuthenticateUserName(userNameToken));
}
return new ValueTask<AuthenticationResult>(AuthenticationResult.NotHandled);
}
private AuthenticationResult AuthenticateUserName(
UserNameIdentityTokenHandler userNameToken)
{
try
{
if (!m_server.VerifyPassword(userNameToken))
{
return AuthenticationResult.Reject(
new ServiceResult(
StatusCodes.BadUserAccessDenied,
new LocalizedText("Invalid username or password.")));
}
IEnumerable<Role> roles = m_server.m_userDatabase.GetUserRoles(userNameToken.UserName);
IReadOnlyList<NodeId>? administeredAppIds =
(m_server.m_userDatabase as IGdsUserDatabase)?
.GetAdministeredApplicationIds(userNameToken.UserName);
var identity = new GdsRoleBasedIdentity(
new UserIdentity(userNameToken),
roles,
default,
administeredAppIds,
m_server.ServerInternal.MessageContext.NamespaceUris);
return AuthenticationResult.Accept(identity);
}
catch (ServiceResultException ex)
{
return AuthenticationResult.Reject(ex.Result);
}
catch (ArgumentException ex)
{
return AuthenticationResult.Reject(
new ServiceResult(
StatusCodes.BadIdentityTokenRejected,
new LocalizedText(ex.Message)));
}
}
}
private sealed class GlobalDiscoverySampleX509Authenticator : IUserTokenAuthenticator
{
private readonly GlobalDiscoverySampleServer m_server;
public GlobalDiscoverySampleX509Authenticator(GlobalDiscoverySampleServer server)
{
m_server = server ?? throw new ArgumentNullException(nameof(server));
}
public UserTokenType TokenType => UserTokenType.Certificate;
public string? IssuedTokenProfileUri => null;
public ValueTask<AuthenticationResult> AuthenticateAsync(
AuthenticationContext context,
CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
if (context.TokenHandler is not X509IdentityTokenHandler x509Token)
{
return new ValueTask<AuthenticationResult>(AuthenticationResult.NotHandled);
}
return new ValueTask<AuthenticationResult>(AuthenticateX509(x509Token));
}
private AuthenticationResult AuthenticateX509(X509IdentityTokenHandler x509Token)
{
try
{
m_server.VerifyX509IdentityToken((X509IdentityToken)x509Token.Token);
var identity = new GdsRoleBasedIdentity(
new UserIdentity(x509Token),
[Role.AuthenticatedUser],
m_server.ServerInternal.MessageContext.NamespaceUris);
m_server.m_logger.LogInformation(
"X509 Token Accepted: {Identity} as {Role}",
identity.DisplayName,
Role.AuthenticatedUser);
return AuthenticationResult.Accept(identity);
}
catch (ServiceResultException ex)
{
return AuthenticationResult.Reject(ex.Result);
}
}
}
private readonly IApplicationsDatabase m_database;
private readonly ICertificateRequest m_request;
private readonly ICertificateGroup m_certificateGroup;
private readonly IUserDatabase m_userDatabase;
private readonly bool m_autoApprove;
private readonly bool m_enableApplicationSelfAdminProvider;
}
}