From d527fddfc14ddbf5ff8fadcf2c9c68338f205fcd Mon Sep 17 00:00:00 2001 From: Marc Schier Date: Wed, 10 Jan 2024 13:28:21 +0100 Subject: [PATCH] Concrete types for credentials (#2154) * Update System.Web.Identity to latest * Fix #2152 --- docs/opc-publisher/definitions.md | 21 ++++- docs/opc-publisher/openapi.json | 28 ++++++- docs/release-announcement.md | 7 +- docs/web-api/definitions.md | 21 ++++- docs/web-api/openapi.json | 28 ++++++- .../src/CredentialModel.cs | 10 ++- .../src/UserIdentityModel.cs | 69 +++++++++++++++++ ...IIoT.OpcUa.Publisher.Service.WebApi.csproj | 2 +- .../src/Stack/Extensions/StackModelsEx.cs | 76 +++++++------------ .../src/Storage/PublishedNodesConverter.cs | 17 ++--- .../tests/Stack/OpcUaApplicationTests.cs | 36 ++++----- .../PublishedNodesJobConverterTests.cs | 20 ++--- .../Publisher/Extensions/ConnectionModelEx.cs | 4 +- .../Publisher/Extensions/CredentialModelEx.cs | 41 +++++++--- .../Extensions/UserIdentityModelEx.cs | 57 ++++++++++++++ 15 files changed, 316 insertions(+), 121 deletions(-) create mode 100644 src/Azure.IIoT.OpcUa.Publisher.Models/src/UserIdentityModel.cs create mode 100644 src/Azure.IIoT.OpcUa/src/Publisher/Extensions/UserIdentityModelEx.cs diff --git a/docs/opc-publisher/definitions.md b/docs/opc-publisher/definitions.md index a507266b15..d2b9a439f4 100644 --- a/docs/opc-publisher/definitions.md +++ b/docs/opc-publisher/definitions.md @@ -389,12 +389,15 @@ Content filter ### CredentialModel -Credential model +Credential model. For backwards compatibility +the actual credentials to pass to the server is set +through the value property. |Name|Schema| |---|---| |**type**
*optional*|[CredentialType](definitions.md#credentialtype)| +|**value**
*optional*|[UserIdentityModel](definitions.md#useridentitymodel)| @@ -733,7 +736,7 @@ Result of GetConfiguredNodesOnEndpoint method call ### HeartbeatBehavior Heartbeat behavior -*Type* : enum (WatchdogLKV, WatchdogLKG, PeriodicLKV, PeriodicLKG, WatchdogLKVWithUpdatedTimestamps) +*Type* : enum (WatchdogLKV, WatchdogLKG, PeriodicLKV, PeriodicLKG, WatchdogLKVWithUpdatedTimestamps, WatchdogLKVDiagnosticsOnly) @@ -2151,6 +2154,18 @@ connection endpoint |**request**
*optional*|[UpdateValuesDetailsModelHistoryUpdateRequestModel](definitions.md#updatevaluesdetailsmodelhistoryupdaterequestmodel)| + +### UserIdentityModel +User identity model + + +|Name|Description|Schema| +|---|---|---| +|**password**
*optional*|

For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.UserName authentication
this is the password of the user.



For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.X509Certificate authentication
this is the passcode to export the configured certificate's
private key.



Not used for the other authentication types.|string| +|**thumbprint**
*optional*|

For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.X509Certificate authentication
this is the thumbprint of the configured certificate to use.
Either Azure.IIoT.OpcUa.Publisher.Models.UserIdentityModel.User or Azure.IIoT.OpcUa.Publisher.Models.UserIdentityModel.Thumbprint must be
used to select the certificate in the user certificate store.



Not used for the other authentication types.|string| +|**user**
*optional*|

For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.UserName authentication
this is the name of the user.



For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.X509Certificate authentication
this is the subject name of the certificate that has been
configured.
Either Azure.IIoT.OpcUa.Publisher.Models.UserIdentityModel.User or Azure.IIoT.OpcUa.Publisher.Models.UserIdentityModel.Thumbprint must be
used to select the certificate in the user certificate store.



Not used for the other authentication types.|string| + + ### ValueReadRequestModel Request node value read @@ -2356,7 +2371,7 @@ Result of attribute write ### WriterGroupTransport Desired writer group transport -*Type* : enum (IoTHub, Mqtt, Dapr, Http, FileSystem) +*Type* : enum (IoTHub, Mqtt, Dapr, Http, FileSystem, Null) diff --git a/docs/opc-publisher/openapi.json b/docs/opc-publisher/openapi.json index cbd9b5e426..c15d0e202d 100644 --- a/docs/opc-publisher/openapi.json +++ b/docs/opc-publisher/openapi.json @@ -3578,14 +3578,14 @@ } }, "CredentialModel": { - "description": "Credential model", + "description": "Credential model. For backwards compatibility\r\nthe actual credentials to pass to the server is set\r\nthrough the value property.", "type": "object", "properties": { "type": { "$ref": "#/definitions/CredentialType" }, "value": { - "description": "Credential to pass to server." + "$ref": "#/definitions/UserIdentityModel" } } }, @@ -4217,7 +4217,8 @@ "WatchdogLKG", "PeriodicLKV", "PeriodicLKG", - "WatchdogLKVWithUpdatedTimestamps" + "WatchdogLKVWithUpdatedTimestamps", + "WatchdogLKVDiagnosticsOnly" ], "type": "string", "x-ms-enum": { @@ -7105,6 +7106,24 @@ } } }, + "UserIdentityModel": { + "description": "User identity model", + "type": "object", + "properties": { + "user": { + "description": "
\r\n For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.UserName authentication\r\n this is the name of the user.\r\n \r\n
\r\n For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.X509Certificate authentication\r\n this is the subject name of the certificate that has been\r\n configured.\r\n Either Azure.IIoT.OpcUa.Publisher.Models.UserIdentityModel.User or Azure.IIoT.OpcUa.Publisher.Models.UserIdentityModel.Thumbprint must be\r\n used to select the certificate in the user certificate store.\r\n \r\n
\r\n Not used for the other authentication types.\r\n ", + "type": "string" + }, + "password": { + "description": "
\r\n For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.UserName authentication\r\n this is the password of the user.\r\n \r\n
\r\n For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.X509Certificate authentication\r\n this is the passcode to export the configured certificate's\r\n private key.\r\n \r\n
\r\n Not used for the other authentication types.\r\n ", + "type": "string" + }, + "thumbprint": { + "description": "
\r\n For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.X509Certificate authentication\r\n this is the thumbprint of the configured certificate to use.\r\n Either Azure.IIoT.OpcUa.Publisher.Models.UserIdentityModel.User or Azure.IIoT.OpcUa.Publisher.Models.UserIdentityModel.Thumbprint must be\r\n used to select the certificate in the user certificate store.\r\n \r\n
\r\n Not used for the other authentication types.\r\n ", + "type": "string" + } + } + }, "ValueReadRequestModel": { "description": "Request node value read", "type": "object", @@ -7446,7 +7465,8 @@ "Mqtt", "Dapr", "Http", - "FileSystem" + "FileSystem", + "Null" ], "type": "string", "x-ms-enum": { diff --git a/docs/release-announcement.md b/docs/release-announcement.md index f1e2aafc9d..d6c2fb7635 100644 --- a/docs/release-announcement.md +++ b/docs/release-announcement.md @@ -54,14 +54,17 @@ We are pleased to announce the release of version 2.9.4 of OPC Publisher and the ### Changes in 2.9.4 +- Send the error of CreateMonitoredItem as part of the keyframe field and in heartbeats if WatchdogLKV heartbeat behavior is used (#2150). +- Credential based authentication uses concrete types for credentials now which are documented in openapi.json (#2152) +- OPC Publisher can now obtain TLS certificates from IoT Edge workload API to secure the HTTPS API (#2101) - Fix release build issue which broke support for ARM64 images running on RPi4 (#2145). -- Update console diagnostics output to provide better naming and reflect other transports than IoT Edge Hub (#2141) +- Update console diagnostics output to provide better naming, additional diagnostics and reflect other transports than IoT Edge Hub (#2141) - Add keep alive notification counts to Diagnostics output and messages - Add a full version that includes runtime, framework and full version string to runtime state message, twin, diagnostic object, and in console output. - When only using cyclic reads, the underlying dummy subscription should stay disabled (#2139) - Recreate session if it expires on server (#2138) - Log subscription keep alive error only when session is connected (#2137) -- Update OPC UA .net stack to latest version (1.4.372.106) to enable fully async reconnect +- Update OPC UA .net stack to latest version (1.4.372.116-preview) to enable fully async reconnect and fix several issues in previous versions. - Fix issue where certain publish errors cause reconnect state machine to fail (#2104, #2136) ## Azure Industrial IoT OPC Publisher 2.9.3 diff --git a/docs/web-api/definitions.md b/docs/web-api/definitions.md index 77a980a65c..5853db6ab0 100644 --- a/docs/web-api/definitions.md +++ b/docs/web-api/definitions.md @@ -371,12 +371,15 @@ Content filter ### CredentialModel -Credential model +Credential model. For backwards compatibility +the actual credentials to pass to the server is set +through the value property. |Name|Schema| |---|---| |**type**
*optional*|[CredentialType](definitions.md#credentialtype)| +|**value**
*optional*|[UserIdentityModel](definitions.md#useridentitymodel)| @@ -802,7 +805,7 @@ Gateway registration update request ### HeartbeatBehavior Heartbeat behavior -*Type* : enum (WatchdogLKV, WatchdogLKG, PeriodicLKV, PeriodicLKG, WatchdogLKVWithUpdatedTimestamps) +*Type* : enum (WatchdogLKV, WatchdogLKG, PeriodicLKV, PeriodicLKG, WatchdogLKVWithUpdatedTimestamps, WatchdogLKVDiagnosticsOnly) @@ -1976,6 +1979,18 @@ Request node history update |**nodeId**
*required*|Node to update
**Minimum length** : `1`|string| + +### UserIdentityModel +User identity model + + +|Name|Description|Schema| +|---|---|---| +|**password**
*optional*|

For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.UserName authentication
this is the password of the user.



For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.X509Certificate authentication
this is the passcode to export the configured certificate's
private key.



Not used for the other authentication types.|string| +|**thumbprint**
*optional*|

For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.X509Certificate authentication
this is the thumbprint of the configured certificate to use.
Either Azure.IIoT.OpcUa.Publisher.Models.UserIdentityModel.User or Azure.IIoT.OpcUa.Publisher.Models.UserIdentityModel.Thumbprint must be
used to select the certificate in the user certificate store.



Not used for the other authentication types.|string| +|**user**
*optional*|

For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.UserName authentication
this is the name of the user.



For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.X509Certificate authentication
this is the subject name of the certificate that has been
configured.
Either Azure.IIoT.OpcUa.Publisher.Models.UserIdentityModel.User or Azure.IIoT.OpcUa.Publisher.Models.UserIdentityModel.Thumbprint must be
used to select the certificate in the user certificate store.



Not used for the other authentication types.|string| + + ### ValueReadRequestModel Request node value read @@ -2116,7 +2131,7 @@ Result of attribute write ### WriterGroupTransport Desired writer group transport -*Type* : enum (IoTHub, Mqtt, Dapr, Http, FileSystem) +*Type* : enum (IoTHub, Mqtt, Dapr, Http, FileSystem, Null) diff --git a/docs/web-api/openapi.json b/docs/web-api/openapi.json index d100392c95..c73ab15f89 100644 --- a/docs/web-api/openapi.json +++ b/docs/web-api/openapi.json @@ -4908,14 +4908,14 @@ } }, "CredentialModel": { - "description": "Credential model", + "description": "Credential model. For backwards compatibility\r\nthe actual credentials to pass to the server is set\r\nthrough the value property.", "type": "object", "properties": { "type": { "$ref": "#/definitions/CredentialType" }, "value": { - "description": "Credential to pass to server." + "$ref": "#/definitions/UserIdentityModel" } } }, @@ -5730,7 +5730,8 @@ "WatchdogLKG", "PeriodicLKV", "PeriodicLKG", - "WatchdogLKVWithUpdatedTimestamps" + "WatchdogLKVWithUpdatedTimestamps", + "WatchdogLKVDiagnosticsOnly" ], "type": "string", "x-ms-enum": { @@ -8259,6 +8260,24 @@ } } }, + "UserIdentityModel": { + "description": "User identity model", + "type": "object", + "properties": { + "user": { + "description": "
\r\n For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.UserName authentication\r\n this is the name of the user.\r\n \r\n
\r\n For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.X509Certificate authentication\r\n this is the subject name of the certificate that has been\r\n configured.\r\n Either Azure.IIoT.OpcUa.Publisher.Models.UserIdentityModel.User or Azure.IIoT.OpcUa.Publisher.Models.UserIdentityModel.Thumbprint must be\r\n used to select the certificate in the user certificate store.\r\n \r\n
\r\n Not used for the other authentication types.\r\n ", + "type": "string" + }, + "password": { + "description": "
\r\n For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.UserName authentication\r\n this is the password of the user.\r\n \r\n
\r\n For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.X509Certificate authentication\r\n this is the passcode to export the configured certificate's\r\n private key.\r\n \r\n
\r\n Not used for the other authentication types.\r\n ", + "type": "string" + }, + "thumbprint": { + "description": "
\r\n For Azure.IIoT.OpcUa.Publisher.Models.CredentialType.X509Certificate authentication\r\n this is the thumbprint of the configured certificate to use.\r\n Either Azure.IIoT.OpcUa.Publisher.Models.UserIdentityModel.User or Azure.IIoT.OpcUa.Publisher.Models.UserIdentityModel.Thumbprint must be\r\n used to select the certificate in the user certificate store.\r\n \r\n
\r\n Not used for the other authentication types.\r\n ", + "type": "string" + } + } + }, "ValueReadRequestModel": { "description": "Request node value read", "type": "object", @@ -8524,7 +8543,8 @@ "Mqtt", "Dapr", "Http", - "FileSystem" + "FileSystem", + "Null" ], "type": "string", "x-ms-enum": { diff --git a/src/Azure.IIoT.OpcUa.Publisher.Models/src/CredentialModel.cs b/src/Azure.IIoT.OpcUa.Publisher.Models/src/CredentialModel.cs index 35c6d6a42b..7cada74b9b 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Models/src/CredentialModel.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Models/src/CredentialModel.cs @@ -5,11 +5,12 @@ namespace Azure.IIoT.OpcUa.Publisher.Models { - using Furly.Extensions.Serializers; using System.Runtime.Serialization; /// - /// Credential model + /// Credential model. For backwards compatibility + /// the actual credentials to pass to the server is set + /// through the value property. /// [DataContract] public sealed record class CredentialModel @@ -22,10 +23,11 @@ public sealed record class CredentialModel public CredentialType? Type { get; set; } /// - /// Credential to pass to server. + /// Credential to pass to server. Can be omitted in case of + /// . /// [DataMember(Name = "value", Order = 1, EmitDefaultValue = false)] - public VariantValue? Value { get; set; } + public UserIdentityModel? Value { get; set; } } } diff --git a/src/Azure.IIoT.OpcUa.Publisher.Models/src/UserIdentityModel.cs b/src/Azure.IIoT.OpcUa.Publisher.Models/src/UserIdentityModel.cs new file mode 100644 index 0000000000..516f89044b --- /dev/null +++ b/src/Azure.IIoT.OpcUa.Publisher.Models/src/UserIdentityModel.cs @@ -0,0 +1,69 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +namespace Azure.IIoT.OpcUa.Publisher.Models +{ + using System.Runtime.Serialization; + + /// + /// User identity model + /// + [DataContract] + public sealed record class UserIdentityModel + { + /// + /// + /// For authentication + /// this is the name of the user. + /// + /// + /// For authentication + /// this is the subject name of the certificate that has been + /// configured. + /// Either or must be + /// used to select the certificate in the user certificate store. + /// + /// + /// Not used for the other authentication types. + /// + /// + [DataMember(Name = "user", Order = 1, + EmitDefaultValue = false)] + public string? User { get; set; } + + /// + /// + /// For authentication + /// this is the password of the user. + /// + /// + /// For authentication + /// this is the passcode to export the configured certificate's + /// private key. + /// + /// + /// Not used for the other authentication types. + /// + /// + [DataMember(Name = "password", Order = 2, + EmitDefaultValue = false)] + public string? Password { get; set; } + + /// + /// + /// For authentication + /// this is the thumbprint of the configured certificate to use. + /// Either or must be + /// used to select the certificate in the user certificate store. + /// + /// + /// Not used for the other authentication types. + /// + /// + [DataMember(Name = "thumbprint", Order = 3, + EmitDefaultValue = false)] + public string? Thumbprint { get; set; } + } +} diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj index 10f8440729..d932331c67 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj @@ -20,7 +20,7 @@ - + diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/StackModelsEx.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/StackModelsEx.cs index 7a990e2cbc..1cd3e92bcc 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/StackModelsEx.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Extensions/StackModelsEx.cs @@ -349,63 +349,41 @@ public static IReadOnlyList ToServiceModel( public static async ValueTask ToUserIdentityAsync( this CredentialModel? credential, ApplicationConfiguration configuration) { - switch (credential?.Type ?? CredentialType.None) + if (credential == null || credential.Type == CredentialType.None) + { + return new UserIdentity(new AnonymousIdentityToken()); + } + var identity = credential.Value; + if (identity == null) + { + throw new ServiceResultException(StatusCodes.BadInvalidArgument, + $"Credential type {credential.Type} requires providing a credential value."); + } + switch (credential.Type) { case CredentialType.UserName: - if (credential!.Value?.IsObject == true && - credential.Value.TryGetProperty("user", out var user) && - user.IsString && - credential.Value.TryGetProperty("password", out var password) && - password.IsString) - { - return new UserIdentity((string?)user, (string?)password); - } - throw new ServiceResultException(StatusCodes.BadNotSupported, - "User/password credential provided is invalid."); + return new UserIdentity(identity.User, identity.Password); case CredentialType.X509Certificate: - if (credential!.Value?.IsObject == true) + string? subjectName = identity.User; + string? thumbprint = identity.Thumbprint; + string? passCode = identity.Password; + if (thumbprint != null || subjectName != null) { - string? subjectName = null; - if (credential.Value.TryGetProperty("user", out user) - && user.IsString) - { - subjectName = (string?)user; - } - string? thumbprint = null; - if (credential.Value.TryGetProperty("thumbprint", out user) - && user.IsString) - { - thumbprint = (string?)user; - } - string? passCode = null; - if (credential.Value.TryGetProperty("password", out password) - && password.IsString) - { - passCode = (string?)password; - } - if (thumbprint != null || subjectName != null) + using var users = configuration.SecurityConfiguration + .TrustedUserCertificates.OpenStore(); + var userCertWithPrivateKey = await users.LoadPrivateKey( + thumbprint, subjectName, passCode).ConfigureAwait(false); + if (userCertWithPrivateKey == null) { - using var users = configuration.SecurityConfiguration - .TrustedUserCertificates.OpenStore(); - var userCertWithPrivateKey = await users.LoadPrivateKey( - thumbprint, subjectName, passCode).ConfigureAwait(false); - if (userCertWithPrivateKey == null) - { - throw new ServiceResultException(StatusCodes.BadCertificateInvalid, - $"User certificate for {subjectName ?? thumbprint} missing " + - "or provided password invalid. Please configure the User " + - "Certificate correctly in the User certificate store."); - } - return new UserIdentity(userCertWithPrivateKey); + throw new ServiceResultException(StatusCodes.BadCertificateInvalid, + $"User certificate for {subjectName ?? thumbprint} missing " + + "or provided password invalid. Please configure the User " + + "Certificate correctly in the User certificate store."); } + return new UserIdentity(userCertWithPrivateKey); } throw new ServiceResultException(StatusCodes.BadNotSupported, - "X509Certificate reference credential format is invalid."); - case CredentialType.JwtToken: - return new UserIdentity(new IssuedIdentityToken - { - DecryptedTokenData = credential!.Value?.ConvertTo() - }); + "X509Certificate credential requires to set either a thumbprint or subject name (user)."); case CredentialType.None: return new UserIdentity(new AnonymousIdentityToken()); default: diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesConverter.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesConverter.cs index 85e29da36f..1c914b0d39 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesConverter.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesConverter.cs @@ -13,6 +13,7 @@ namespace Azure.IIoT.OpcUa.Publisher.Storage using Furly.Extensions.Serializers; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; + using Opc.Ua; using System; using System.Collections.Generic; using System.Diagnostics; @@ -556,18 +557,10 @@ private PublishedNodesEntryModel AddConnectionModel(ConnectionModel? connection, return publishedNodesEntryModel; async Task<(string? user, string? password, bool encrypted)> ToUserNamePasswordCredentialAsync( - VariantValue? credential) + UserIdentityModel? credential) { - if (credential == null || !credential.TryGetProperty("user", out var user) || - !user.TryGetString(out var userString, false, CultureInfo.InvariantCulture)) - { - userString = string.Empty; - } - if (credential == null || !credential.TryGetProperty("password", out var password) || - !password.TryGetString(out var passwordString, false, CultureInfo.InvariantCulture)) - { - passwordString = string.Empty; - } + var userString = credential?.User ?? string.Empty; + var passwordString = credential?.Password ?? string.Empty; if (_forceCredentialEncryption) { @@ -861,7 +854,7 @@ private async Task ToCredentialAsync(PublishedNodesEntryModel e Type = entry.OpcAuthenticationMode == OpcAuthenticationMode.Certificate ? CredentialType.X509Certificate : CredentialType.UserName, - Value = _serializer.FromObject(new { user, password }) + Value = new UserIdentityModel { User = user, Password = password } }; } return new CredentialModel diff --git a/src/Azure.IIoT.OpcUa.Publisher/tests/Stack/OpcUaApplicationTests.cs b/src/Azure.IIoT.OpcUa.Publisher/tests/Stack/OpcUaApplicationTests.cs index 21ae080dd7..2b90da53a5 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/tests/Stack/OpcUaApplicationTests.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/tests/Stack/OpcUaApplicationTests.cs @@ -260,11 +260,11 @@ await certs.AddCertificateAsync(CertificateStoreName.User, var credential = new CredentialModel { Type = CredentialType.X509Certificate, - Value = new DefaultJsonSerializer().FromObject(new + Value = new UserIdentityModel { - user = "DC=user2", - password = p2 - }) + User = "DC=user2", + Password = p2 + } }; var identity = await credential.ToUserIdentityAsync(config.Value); Assert.NotNull(identity); @@ -305,11 +305,11 @@ await certs.AddCertificateAsync(CertificateStoreName.User, var credential = new CredentialModel { Type = CredentialType.X509Certificate, - Value = new DefaultJsonSerializer().FromObject(new + Value = new UserIdentityModel { - thumbprint = newCert3.Thumbprint, - password = p3 - }) + Thumbprint = newCert3.Thumbprint, + Password = p3 + } }; var identity = await credential.ToUserIdentityAsync(config.Value); Assert.NotNull(identity); @@ -350,11 +350,11 @@ await certs.AddCertificateAsync(CertificateStoreName.User, var credential = new CredentialModel { Type = CredentialType.X509Certificate, - Value = new DefaultJsonSerializer().FromObject(new + Value = new UserIdentityModel { - thumbprint = newCert3.Thumbprint, - password = p1 - }) + Thumbprint = newCert3.Thumbprint, + Password = p1 + } }; var ex = await Assert.ThrowsAsync( async () => await credential.ToUserIdentityAsync(config.Value)); @@ -364,10 +364,10 @@ await certs.AddCertificateAsync(CertificateStoreName.User, credential = new CredentialModel { Type = CredentialType.X509Certificate, - Value = new DefaultJsonSerializer().FromObject(new + Value = new UserIdentityModel { - password = p3 - }) + Password = p3 + } }; ex = await Assert.ThrowsAsync( async () => await credential.ToUserIdentityAsync(config.Value)); @@ -377,10 +377,10 @@ await certs.AddCertificateAsync(CertificateStoreName.User, credential = new CredentialModel { Type = CredentialType.X509Certificate, - Value = new DefaultJsonSerializer().FromObject(new + Value = new UserIdentityModel { - thumbprint = newCert3.Thumbprint, - }) + Thumbprint = newCert3.Thumbprint + } }; ex = await Assert.ThrowsAsync( async () => await credential.ToUserIdentityAsync(config.Value)); diff --git a/src/Azure.IIoT.OpcUa.Publisher/tests/Storage/PublishedNodesJobConverterTests.cs b/src/Azure.IIoT.OpcUa.Publisher/tests/Storage/PublishedNodesJobConverterTests.cs index 016dce78db..cc3b40eb03 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/tests/Storage/PublishedNodesJobConverterTests.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/tests/Storage/PublishedNodesJobConverterTests.cs @@ -86,8 +86,8 @@ public void PnPlcPlainTextUserNamePasswordTest(bool withCryptoProvider, bool pro var credential = Assert.Single(group.DataSetWriters).DataSet?.DataSetSource?.Connection?.User; Assert.NotNull(credential); Assert.Equal(CredentialType.UserName, credential.Type); - Assert.Equal("OpcAuthenticationPassword", credential.Value["password"]); - Assert.Equal("OpcAuthenticationUsername", credential.Value["user"]); + Assert.Equal("OpcAuthenticationPassword", credential.Value.Password); + Assert.Equal("OpcAuthenticationUsername", credential.Value.User); entries = converter.ToPublishedNodes(3, DateTime.UtcNow, writerGroups); entry = Assert.Single(entries); @@ -137,8 +137,8 @@ public void PnPlcPlainTextUserNamePasswordWithCryptoProviderForceEncryptionTest( var credential = Assert.Single(group.DataSetWriters).DataSet?.DataSetSource?.Connection?.User; Assert.NotNull(credential); Assert.Equal(CredentialType.UserName, credential.Type); - Assert.Equal("DecryptedAuthPassword", credential.Value["password"]); - Assert.Equal("DecryptedAuthUsername", credential.Value["user"]); + Assert.Equal("DecryptedAuthPassword", credential.Value.Password); + Assert.Equal("DecryptedAuthUsername", credential.Value.User); entries = converter.ToPublishedNodes(3, DateTime.UtcNow, writerGroups); entry = Assert.Single(entries); @@ -286,8 +286,8 @@ public void PnPlcEncryptedUserNamePasswordWithCryptoProviderTest() var credential = Assert.Single(group.DataSetWriters).DataSet?.DataSetSource?.Connection?.User; Assert.NotNull(credential); Assert.Equal(CredentialType.UserName, credential.Type); - Assert.Equal("DecryptedAuthPassword", credential.Value["password"]); - Assert.Equal("DecryptedAuthUsername", credential.Value["user"]); + Assert.Equal("DecryptedAuthPassword", credential.Value.Password); + Assert.Equal("DecryptedAuthUsername", credential.Value.User); // Now we should have converted back to plain text entries = converter.ToPublishedNodes(3, DateTime.UtcNow, writerGroups); @@ -340,8 +340,8 @@ public void PnPlcEncryptedUserNamePasswordWithCryptoProviderAndForceEncryptionTe var credential = Assert.Single(group.DataSetWriters).DataSet?.DataSetSource?.Connection?.User; Assert.NotNull(credential); Assert.Equal(CredentialType.UserName, credential.Type); - Assert.Equal("DecryptedAuthPassword", credential.Value["password"]); - Assert.Equal("DecryptedAuthUsername", credential.Value["user"]); + Assert.Equal("DecryptedAuthPassword", credential.Value.Password); + Assert.Equal("DecryptedAuthUsername", credential.Value.User); // Now we should have converted back to encrypted entries = converter.ToPublishedNodes(3, DateTime.UtcNow, writerGroups); @@ -403,7 +403,7 @@ public void PnPlcEncryptedUserNamePasswordWithoutCryptoProviderAndForceEncryptio Assert.Null(credential.Value); // Fake credential - credential.Value = _serializer.FromObject(new { user = "user", password = "password" }); + credential.Value = new UserIdentityModel { User = "user", Password = "password" }; credential.Type = CredentialType.UserName; entries = converter.ToPublishedNodes(3, DateTime.UtcNow, writerGroups).ToList(); Assert.True(failFastCalled); // Process exited @@ -466,7 +466,7 @@ public void PnPlcEncryptedUserNamePasswordWithoutThrowingCryptoProviderAndForceE // Now we should have converted back to encrypted // Fake credential - credential.Value = _serializer.FromObject(new { user = "user", password = "password" }); + credential.Value = new UserIdentityModel { User = "user", Password = "password" }; credential.Type = CredentialType.UserName; entries = converter.ToPublishedNodes(3, DateTime.UtcNow, writerGroups).ToList(); Assert.True(failFastCalled); // Process exited diff --git a/src/Azure.IIoT.OpcUa/src/Publisher/Extensions/ConnectionModelEx.cs b/src/Azure.IIoT.OpcUa/src/Publisher/Extensions/ConnectionModelEx.cs index 284914b97e..f4d0ea38bc 100644 --- a/src/Azure.IIoT.OpcUa/src/Publisher/Extensions/ConnectionModelEx.cs +++ b/src/Azure.IIoT.OpcUa/src/Publisher/Extensions/ConnectionModelEx.cs @@ -48,7 +48,7 @@ public static bool IsSameAs(this ConnectionModel? model, ConnectionModel? that) { return false; } - if (!VariantValue.DeepEquals(that.User?.Value, model.User?.Value)) + if (!that.User.IsSameAs(model.User)) { return false; } @@ -117,7 +117,7 @@ public static int CreateConsistentHash(this ConnectionModel model) hashCode = (hashCode * -1521134295) + model.Endpoint?.CreateConsistentHash() ?? 0; hashCode = (hashCode * -1521134295) + - EqualityComparer.Default.GetHashCode(model.User?.Value ?? VariantValue.Null); + EqualityComparer.Default.GetHashCode(model.User ?? new CredentialModel()); hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(model.Diagnostics?.AuditId ?? string.Empty); hashCode = (hashCode * -1521134295) + diff --git a/src/Azure.IIoT.OpcUa/src/Publisher/Extensions/CredentialModelEx.cs b/src/Azure.IIoT.OpcUa/src/Publisher/Extensions/CredentialModelEx.cs index f8524b0dd8..2e7b3a5863 100644 --- a/src/Azure.IIoT.OpcUa/src/Publisher/Extensions/CredentialModelEx.cs +++ b/src/Azure.IIoT.OpcUa/src/Publisher/Extensions/CredentialModelEx.cs @@ -20,11 +20,9 @@ public static class CredentialModelEx public static string? GetPassword(this CredentialModel? model) { if (model?.Type == CredentialType.UserName && - model.Value?.IsObject == true && - model.Value.TryGetProperty("password", out var password) && - password.IsString) + model.Value != null) { - return (string?)password; + return model.Value.Password; } return null; } @@ -37,15 +35,40 @@ public static class CredentialModelEx public static string? GetUserName(this CredentialModel? model) { if (model?.Type == CredentialType.UserName && - model.Value?.IsObject == true && - model.Value.TryGetProperty("user", out var user) && - user.IsString) + model.Value != null) { - return (string?)user; + return model.Value.User; } return null; } + /// + /// Equality comparison + /// + /// + /// + /// + public static bool IsSameAs(this CredentialModel? model, CredentialModel? that) + { + if (model == that) + { + return true; + } + + model ??= new CredentialModel(); + that ??= new CredentialModel(); + + if (that.Type != model.Type) + { + return false; + } + if (!that.Value.IsSameAs(model.Value)) + { + return false; + } + return true; + } + /// /// Deep clone /// @@ -56,7 +79,7 @@ public static class CredentialModelEx { return model == null ? null : (model with { - Value = model.Value?.Copy() + Value = model.Value.Clone() }); } } diff --git a/src/Azure.IIoT.OpcUa/src/Publisher/Extensions/UserIdentityModelEx.cs b/src/Azure.IIoT.OpcUa/src/Publisher/Extensions/UserIdentityModelEx.cs new file mode 100644 index 0000000000..d06de327c4 --- /dev/null +++ b/src/Azure.IIoT.OpcUa/src/Publisher/Extensions/UserIdentityModelEx.cs @@ -0,0 +1,57 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +namespace Azure.IIoT.OpcUa.Publisher.Models +{ + using System.Diagnostics.CodeAnalysis; + + /// + /// User Identity model extensions + /// + public static class UserIdentityModelEx + { + /// + /// Equality comparison + /// + /// + /// + /// + public static bool IsSameAs(this UserIdentityModel? model, UserIdentityModel? that) + { + if (model == that) + { + return true; + } + + model ??= new UserIdentityModel(); + that ??= new UserIdentityModel(); + + if ((that.User ?? string.Empty) != (model.User ?? string.Empty)) + { + return false; + } + if ((that.Password ?? string.Empty) != (model.Password ?? string.Empty)) + { + return false; + } + if ((that.Thumbprint ?? string.Empty) != (model.Thumbprint ?? string.Empty)) + { + return false; + } + return true; + } + + /// + /// Deep clone + /// + /// + /// + [return: NotNullIfNotNull(nameof(model))] + public static UserIdentityModel? Clone(this UserIdentityModel? model) + { + return model == null ? null : (model with { }); + } + } +}