mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-26 12:40:03 +08:00
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.
488 lines
20 KiB
C#
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;
|
|
}
|
|
}
|