From 5dbb7ddb85b6482846b979e5c8789e5aea06f6ed Mon Sep 17 00:00:00 2001 From: Azad Abbasi Date: Wed, 26 Jan 2022 10:12:37 -0800 Subject: [PATCH 1/6] Revert "Revert "Adding RBAC support for provisioning SDK (#2262)"" This reverts commit 1a0b9de942fff85cbc76384da297914f71cdcce0. --- .../src/Auth/ProvisioningSasCredential.cs | 27 +++ .../src/Auth/ProvisioningTokenCredential.cs | 43 +++++ provisioning/service/src/Auth/TokenHelper.cs | 21 +++ .../src/Manager/EnrollmentGroupManager.cs | 5 +- .../Manager/IndividualEnrollmentManager.cs | 5 +- .../src/Manager/RegistrationStatusManager.cs | 6 +- ....Azure.Devices.Provisioning.Service.csproj | 1 + .../service/src/ProvisioningServiceClient.cs | 168 ++++++++++++++++-- provisioning/service/src/Query.cs | 65 +++++-- 9 files changed, 298 insertions(+), 43 deletions(-) create mode 100644 provisioning/service/src/Auth/ProvisioningSasCredential.cs create mode 100644 provisioning/service/src/Auth/ProvisioningTokenCredential.cs create mode 100644 provisioning/service/src/Auth/TokenHelper.cs diff --git a/provisioning/service/src/Auth/ProvisioningSasCredential.cs b/provisioning/service/src/Auth/ProvisioningSasCredential.cs new file mode 100644 index 0000000000..c003148f6c --- /dev/null +++ b/provisioning/service/src/Auth/ProvisioningSasCredential.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Azure; +using Microsoft.Azure.Devices.Common.Service.Auth; + +namespace Microsoft.Azure.Devices.Provisioning.Service.Auth +{ + /// + /// Allows authentication to the API using a Shared Access Key provided by custom implementation. + /// The PnP client is auto generated from swagger and needs to implement a specific class to pass to the protocol layer + /// unlike the rest of the clients which are hand-written. So, this implementation for authentication is specific to digital twin (PnP). + /// + internal class ProvisioningSasCredential: IAuthorizationHeaderProvider + { + private readonly AzureSasCredential _azureSasCredential; + + public ProvisioningSasCredential(AzureSasCredential azureSasCredential) + { + _azureSasCredential = azureSasCredential; + } + + public string GetAuthorizationHeader() + { + return _azureSasCredential.Signature; + } + } +} diff --git a/provisioning/service/src/Auth/ProvisioningTokenCredential.cs b/provisioning/service/src/Auth/ProvisioningTokenCredential.cs new file mode 100644 index 0000000000..fe05a70ae6 --- /dev/null +++ b/provisioning/service/src/Auth/ProvisioningTokenCredential.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Threading; +using Azure.Core; +using Microsoft.Azure.Devices.Common.Service.Auth; + +namespace Microsoft.Azure.Devices.Provisioning.Service.Auth +{ + /// + /// Allows authentication to the API using a JWT token generated for Azure active directory. + /// The PnP client is auto generated from swagger and needs to implement a specific class to pass to the protocol layer + /// unlike the rest of the clients which are hand-written. so, this implementation for authentication is specific to digital twin (PnP). + /// + internal class ProvisioningTokenCredential : IAuthorizationHeaderProvider + { + private readonly TokenCredential _credential; + private readonly object _tokenLock = new object(); + private AccessToken? _cachedAccessToken; + + public ProvisioningTokenCredential(TokenCredential credential) + { + _credential = credential; + } + + // The HTTP protocol uses this method to get the bearer token for authentication. + public string GetAuthorizationHeader() + { + lock (_tokenLock) + { + // A new token is generated if it is the first time or the cached token is close to expiry. + if (!_cachedAccessToken.HasValue + || TokenHelper.IsCloseToExpiry(_cachedAccessToken.Value.ExpiresOn)) + { + _cachedAccessToken = _credential.GetToken( + new TokenRequestContext(new string[] { "https://azure-devices-provisioning.net/.default" }), + new CancellationToken()); + } + } + + return $"Bearer {_cachedAccessToken.Value.Token}"; + } + } +} diff --git a/provisioning/service/src/Auth/TokenHelper.cs b/provisioning/service/src/Auth/TokenHelper.cs new file mode 100644 index 0000000000..f98c07eac3 --- /dev/null +++ b/provisioning/service/src/Auth/TokenHelper.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; + +namespace Microsoft.Azure.Devices.Provisioning.Service.Auth +{ + internal static class TokenHelper + { + /// + /// Determines if the given token expiry date time is close to expiry. The date and time is + /// considered close to expiry if it has less than 10 minutes relative to the current time. + /// + /// The token expiration date and time. + /// True if the token expiry has less than 10 minutes relative to the current time, otherwise false. + public static bool IsCloseToExpiry(DateTimeOffset expiry) + { + TimeSpan timeToExpiry = expiry - DateTimeOffset.UtcNow; + return timeToExpiry.TotalMinutes < 10; + } + } +} diff --git a/provisioning/service/src/Manager/EnrollmentGroupManager.cs b/provisioning/service/src/Manager/EnrollmentGroupManager.cs index 9cddfe1fe7..08052420f1 100644 --- a/provisioning/service/src/Manager/EnrollmentGroupManager.cs +++ b/provisioning/service/src/Manager/EnrollmentGroupManager.cs @@ -109,7 +109,8 @@ await contractApiHttp.RequestAsync( } internal static Query CreateQuery( - ServiceConnectionString provisioningConnectionString, + string hostName, + IAuthorizationHeaderProvider headerProvider, QuerySpecification querySpecification, HttpTransportSettings httpTransportSettings, CancellationToken cancellationToken, @@ -128,7 +129,7 @@ internal static Query CreateQuery( /* SRS_ENROLLMENT_GROUP_MANAGER_28_015: [The CreateQuery shall return a new Query for EnrollmentGroup.] */ - return new Query(provisioningConnectionString, ServiceName, querySpecification, httpTransportSettings, pageSize, cancellationToken); + return new Query(hostName, headerProvider, ServiceName, querySpecification, httpTransportSettings, pageSize, cancellationToken); } private static Uri GetEnrollmentUri(string enrollmentGroupId) diff --git a/provisioning/service/src/Manager/IndividualEnrollmentManager.cs b/provisioning/service/src/Manager/IndividualEnrollmentManager.cs index c4c5d40c22..35f9bad5e3 100644 --- a/provisioning/service/src/Manager/IndividualEnrollmentManager.cs +++ b/provisioning/service/src/Manager/IndividualEnrollmentManager.cs @@ -150,7 +150,8 @@ await contractApiHttp.RequestAsync( } internal static Query CreateQuery( - ServiceConnectionString provisioningConnectionString, + string hostName, + IAuthorizationHeaderProvider headerProvider, QuerySpecification querySpecification, HttpTransportSettings httpTransportSettings, CancellationToken cancellationToken, @@ -168,7 +169,7 @@ internal static Query CreateQuery( } /* SRS_INDIVIDUAL_ENROLLMENT_MANAGER_21_015: [The CreateQuery shall return a new Query for IndividualEnrollments.] */ - return new Query(provisioningConnectionString, ServiceName, querySpecification, httpTransportSettings, pageSize, cancellationToken); + return new Query(hostName, headerProvider, ServiceName, querySpecification, httpTransportSettings, pageSize, cancellationToken); } private static Uri GetEnrollmentUri(string registrationId) diff --git a/provisioning/service/src/Manager/RegistrationStatusManager.cs b/provisioning/service/src/Manager/RegistrationStatusManager.cs index cd786744ff..044b7128f2 100644 --- a/provisioning/service/src/Manager/RegistrationStatusManager.cs +++ b/provisioning/service/src/Manager/RegistrationStatusManager.cs @@ -82,7 +82,8 @@ await contractApiHttp.RequestAsync( [SuppressMessage("Microsoft.Design", "CA1068", Justification = "Public API cannot change parameter order.")] internal static Query CreateEnrollmentGroupQuery( - ServiceConnectionString provisioningConnectionString, + string hostName, + IAuthorizationHeaderProvider headerProvider, QuerySpecification querySpecification, HttpTransportSettings httpTransportSettings, CancellationToken cancellationToken, @@ -102,7 +103,8 @@ internal static Query CreateEnrollmentGroupQuery( /* SRS_REGISTRATION_STATUS_MANAGER_28_010: [The CreateQuery shall return a new Query for DeviceRegistrationState.] */ return new Query( - provisioningConnectionString, + hostName, + headerProvider, GetGetDeviceRegistrationStatus(enrollmentGroupId), querySpecification, httpTransportSettings, diff --git a/provisioning/service/src/Microsoft.Azure.Devices.Provisioning.Service.csproj b/provisioning/service/src/Microsoft.Azure.Devices.Provisioning.Service.csproj index d8d1c5ef0a..dd863daac7 100644 --- a/provisioning/service/src/Microsoft.Azure.Devices.Provisioning.Service.csproj +++ b/provisioning/service/src/Microsoft.Azure.Devices.Provisioning.Service.csproj @@ -56,6 +56,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/provisioning/service/src/ProvisioningServiceClient.cs b/provisioning/service/src/ProvisioningServiceClient.cs index df4c53e24e..216627b733 100644 --- a/provisioning/service/src/ProvisioningServiceClient.cs +++ b/provisioning/service/src/ProvisioningServiceClient.cs @@ -5,7 +5,10 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Azure; +using Azure.Core; using Microsoft.Azure.Devices.Common.Service.Auth; +using Microsoft.Azure.Devices.Provisioning.Service.Auth; namespace Microsoft.Azure.Devices.Provisioning.Service { @@ -65,8 +68,12 @@ namespace Microsoft.Azure.Devices.Provisioning.Service public class ProvisioningServiceClient : IDisposable { private readonly ServiceConnectionString _provisioningConnectionString; + private readonly ProvisioningTokenCredential _provisioningTokenCredential; + private readonly ProvisioningSasCredential _provisioningSasCredential; private readonly IContractApiHttp _contractApiHttp; + private string _hostName; + /// /// Create a new instance of the ProvisioningServiceClient that exposes /// the API to the Device Provisioning Service. @@ -104,6 +111,75 @@ public static ProvisioningServiceClient CreateFromConnectionString(string connec return new ProvisioningServiceClient(connectionString, httpTransportSettings); } + /// + /// Create a new instance of the ProvisioningServiceClient from the provided hostName and TokenCredential + /// that exposes the API to the Device Provisioning Service. + /// + /// + /// The string that carries the hostName that will be used for this object + /// The TokenCredential that provides authentication for this object + /// The ProvisioningServiceClient with the new instance of this object. + /// if the credential is null + public static ProvisioningServiceClient Create(string hostName, TokenCredential credential) + { + return ProvisioningServiceClient.Create(hostName, credential, new HttpTransportSettings()); + } + + /// + /// Create a new instance of the ProvisioningServiceClient from the provided hostName and TokenCredential + /// that exposes the API to the Device Provisioning Service. + /// + /// + /// The string that carries the hostName that will be used for this object + /// The TokenCredential that provides authentication for this object + /// Specifies the HTTP transport settings for the request + /// The ProvisioningServiceClient with the new instance of this object. + /// if the credential is null + public static ProvisioningServiceClient Create(string hostName, TokenCredential credential, HttpTransportSettings httpTransportSettings) + { + if (credential == null) + { + throw new ArgumentNullException(nameof(credential), "Parameter cannot be null"); + } + + return new ProvisioningServiceClient(hostName, credential, httpTransportSettings); + } + + /// + /// Create a new instance of the ProvisioningServiceClient from the provided hostName and TokenCredential + /// that exposes the API to the Device Provisioning Service. + /// + /// + /// The string that carries the host name that will be used for this object + /// The AzureSasCredential that provides authentication for this object + /// The ProvisioningServiceClient with the new instance of this object. + /// if the azureSasCredential is null + public static ProvisioningServiceClient Create(string hostName, AzureSasCredential azureSasCredential) + { + return ProvisioningServiceClient.Create(hostName, azureSasCredential, new HttpTransportSettings()); + } + + /// + /// Create a new instance of the ProvisioningServiceClient from the provided hostName and TokenCredential + /// that exposes the API to the Device Provisioning Service. + /// + /// + /// The string that carries the host name that will be used for this object + /// The AzureSasCredential that provides authentication for this object + /// Specifies the HTTP transport settings for the request + /// The ProvisioningServiceClient with the new instance of this object. + /// if the azureSasCredential is null + public static ProvisioningServiceClient Create(string hostName, AzureSasCredential azureSasCredential, HttpTransportSettings httpTransportSettings) + { + if (azureSasCredential == null) + { + throw new ArgumentNullException(nameof(azureSasCredential), "Parameter cannot be null"); + } + + return new ProvisioningServiceClient(hostName, azureSasCredential, httpTransportSettings); + } + + /// /// PRIVATE CONSTRUCTOR /// @@ -121,12 +197,43 @@ private ProvisioningServiceClient(string connectionString, HttpTransportSettings /* SRS_PROVISIONING_SERVICE_CLIENT_21_003: [The constructor shall throw ArgumentException if the ProvisioningConnectionString or one of the inner Managers failed to create a new instance.] */ /* SRS_PROVISIONING_SERVICE_CLIENT_21_004: [The constructor shall create a new instance of the ContractApiHttp class using the provided connectionString.] */ _provisioningConnectionString = ServiceConnectionString.Parse(connectionString); + _hostName = _provisioningConnectionString.HostName; _contractApiHttp = new ContractApiHttp( _provisioningConnectionString.HttpsEndpoint, _provisioningConnectionString, httpTransportSettings); } + private ProvisioningServiceClient(string hostName, TokenCredential credential, HttpTransportSettings httpTransportSettings) + { + if (string.IsNullOrWhiteSpace(hostName ?? throw new ArgumentNullException(nameof(hostName)))) + { + throw new ArgumentException($"{nameof(hostName)} cannot be empty string"); + } + + _provisioningTokenCredential = new ProvisioningTokenCredential(credential); + _hostName = hostName; + _contractApiHttp = new ContractApiHttp( + new UriBuilder("https", _hostName).Uri, + _provisioningTokenCredential, + httpTransportSettings); + } + + private ProvisioningServiceClient(string hostName, AzureSasCredential azureSasCredential, HttpTransportSettings httpTransportSettings) + { + if (string.IsNullOrWhiteSpace(hostName ?? throw new ArgumentNullException(nameof(hostName)))) + { + throw new ArgumentException($"{nameof(hostName)} cannot be empty string"); + } + + _provisioningSasCredential = new ProvisioningSasCredential(azureSasCredential); + _hostName = hostName; + _contractApiHttp = new ContractApiHttp( + new UriBuilder("https", _hostName).Uri, + _provisioningSasCredential, + httpTransportSettings); + } + /// /// Dispose the Provisioning Service Client and its dependencies. /// @@ -472,7 +579,8 @@ public Query CreateIndividualEnrollmentQuery(QuerySpecification querySpecificati { /* SRS_PROVISIONING_SERVICE_CLIENT_21_014: [The CreateIndividualEnrollmentQuery shall create a new individual enrollment query by calling the CreateQuery in the IndividualEnrollmentManager.] */ return IndividualEnrollmentManager.CreateQuery( - _provisioningConnectionString, + _hostName, + GetHeaderProvider(), querySpecification, new HttpTransportSettings(), CancellationToken.None); @@ -496,7 +604,8 @@ public Query CreateIndividualEnrollmentQuery(QuerySpecification querySpecificati { /* SRS_PROVISIONING_SERVICE_CLIENT_21_014: [The CreateIndividualEnrollmentQuery shall create a new individual enrollment query by calling the CreateQuery in the IndividualEnrollmentManager.] */ return IndividualEnrollmentManager.CreateQuery( - _provisioningConnectionString, + _hostName, + GetHeaderProvider(), querySpecification, httpTransportSettings, CancellationToken.None); @@ -520,7 +629,8 @@ public Query CreateIndividualEnrollmentQuery(QuerySpecification querySpecificati { /* SRS_PROVISIONING_SERVICE_CLIENT_21_014: [The CreateIndividualEnrollmentQuery shall create a new individual enrollment query by calling the CreateQuery in the IndividualEnrollmentManager.] */ return IndividualEnrollmentManager.CreateQuery( - _provisioningConnectionString, + _hostName, + GetHeaderProvider(), querySpecification, new HttpTransportSettings(), cancellationToken); @@ -548,7 +658,8 @@ public Query CreateIndividualEnrollmentQuery(QuerySpecification querySpecificati { /* SRS_PROVISIONING_SERVICE_CLIENT_21_015: [The CreateIndividualEnrollmentQuery shall create a new individual enrollment query by calling the CreateQuery in the IndividualEnrollmentManager.] */ return IndividualEnrollmentManager.CreateQuery( - _provisioningConnectionString, + _hostName, + GetHeaderProvider(), querySpecification, new HttpTransportSettings(), CancellationToken.None, @@ -578,7 +689,8 @@ public Query CreateIndividualEnrollmentQuery(QuerySpecification querySpecificati { /* SRS_PROVISIONING_SERVICE_CLIENT_21_015: [The CreateIndividualEnrollmentQuery shall create a new individual enrollment query by calling the CreateQuery in the IndividualEnrollmentManager.] */ return IndividualEnrollmentManager.CreateQuery( - _provisioningConnectionString, + _hostName, + GetHeaderProvider(), querySpecification, new HttpTransportSettings(), cancellationToken, @@ -608,7 +720,8 @@ public Query CreateIndividualEnrollmentQuery(QuerySpecification querySpecificati { /* SRS_PROVISIONING_SERVICE_CLIENT_21_015: [The CreateIndividualEnrollmentQuery shall create a new individual enrollment query by calling the CreateQuery in the IndividualEnrollmentManager.] */ return IndividualEnrollmentManager.CreateQuery( - _provisioningConnectionString, + _hostName, + GetHeaderProvider(), querySpecification, httpTransportSettings, CancellationToken.None, @@ -855,7 +968,8 @@ public Query CreateEnrollmentGroupQuery(QuerySpecification querySpecification) { /* SRS_PROVISIONING_SERVICE_CLIENT_21_021: [The createEnrollmentGroupQuery shall create a new enrolmentGroup query by calling the createQuery in the EnrollmentGroupManager.] */ return EnrollmentGroupManager.CreateQuery( - _provisioningConnectionString, + _hostName, + GetHeaderProvider(), querySpecification, new HttpTransportSettings(), CancellationToken.None); @@ -879,7 +993,8 @@ public Query CreateEnrollmentGroupQuery(QuerySpecification querySpecification, H { /* SRS_PROVISIONING_SERVICE_CLIENT_21_021: [The createEnrollmentGroupQuery shall create a new enrolmentGroup query by calling the createQuery in the EnrollmentGroupManager.] */ return EnrollmentGroupManager.CreateQuery( - _provisioningConnectionString, + _hostName, + GetHeaderProvider(), querySpecification, httpTransportSettings, CancellationToken.None); @@ -903,7 +1018,8 @@ public Query CreateEnrollmentGroupQuery(QuerySpecification querySpecification, C { /* SRS_PROVISIONING_SERVICE_CLIENT_21_021: [The createEnrollmentGroupQuery shall create a new enrolmentGroup query by calling the createQuery in the EnrollmentGroupManager.] */ return EnrollmentGroupManager.CreateQuery( - _provisioningConnectionString, + _hostName, + GetHeaderProvider(), querySpecification, new HttpTransportSettings(), cancellationToken); @@ -931,7 +1047,8 @@ public Query CreateEnrollmentGroupQuery(QuerySpecification querySpecification, i { /* SRS_PROVISIONING_SERVICE_CLIENT_21_022: [The createEnrollmentGroupQuery shall create a new enrolmentGroup query by calling the createQuery in the EnrollmentGroupManager.] */ return EnrollmentGroupManager.CreateQuery( - _provisioningConnectionString, + _hostName, + GetHeaderProvider(), querySpecification, new HttpTransportSettings(), CancellationToken.None, @@ -961,7 +1078,8 @@ public Query CreateEnrollmentGroupQuery(QuerySpecification querySpecification, i { /* SRS_PROVISIONING_SERVICE_CLIENT_21_022: [The createEnrollmentGroupQuery shall create a new enrolmentGroup query by calling the createQuery in the EnrollmentGroupManager.] */ return EnrollmentGroupManager.CreateQuery( - _provisioningConnectionString, + _hostName, + GetHeaderProvider(), querySpecification, new HttpTransportSettings(), cancellationToken, @@ -991,7 +1109,8 @@ public Query CreateEnrollmentGroupQuery(QuerySpecification querySpecification, i { /* SRS_PROVISIONING_SERVICE_CLIENT_21_022: [The createEnrollmentGroupQuery shall create a new enrolmentGroup query by calling the createQuery in the EnrollmentGroupManager.] */ return EnrollmentGroupManager.CreateQuery( - _provisioningConnectionString, + _hostName, + GetHeaderProvider(), querySpecification, httpTransportSettings, CancellationToken.None, @@ -1178,7 +1297,8 @@ public Query CreateEnrollmentGroupRegistrationStateQuery(QuerySpecification quer { /* SRS_PROVISIONING_SERVICE_CLIENT_21_027: [The createEnrollmentGroupRegistrationStateQuery shall create a new DeviceRegistrationState query by calling the createQuery in the registrationStatusManager.] */ return RegistrationStatusManager.CreateEnrollmentGroupQuery( - _provisioningConnectionString, + _hostName, + GetHeaderProvider(), querySpecification, new HttpTransportSettings(), CancellationToken.None, @@ -1203,7 +1323,8 @@ public Query CreateEnrollmentGroupRegistrationStateQuery(QuerySpecification quer { /* SRS_PROVISIONING_SERVICE_CLIENT_21_027: [The createEnrollmentGroupRegistrationStateQuery shall create a new DeviceRegistrationState query by calling the createQuery in the registrationStatusManager.] */ return RegistrationStatusManager.CreateEnrollmentGroupQuery( - _provisioningConnectionString, + _hostName, + GetHeaderProvider(), querySpecification, httpTransportSettings, CancellationToken.None, @@ -1231,7 +1352,8 @@ public Query CreateEnrollmentGroupRegistrationStateQuery( { /* SRS_PROVISIONING_SERVICE_CLIENT_21_027: [The createEnrollmentGroupRegistrationStateQuery shall create a new DeviceRegistrationState query by calling the createQuery in the registrationStatusManager.] */ return RegistrationStatusManager.CreateEnrollmentGroupQuery( - _provisioningConnectionString, + _hostName, + GetHeaderProvider(), querySpecification, new HttpTransportSettings(), cancellationToken, @@ -1261,7 +1383,8 @@ public Query CreateEnrollmentGroupRegistrationStateQuery(QuerySpecification quer { /* SRS_PROVISIONING_SERVICE_CLIENT_21_028: [The createEnrollmentGroupRegistrationStateQuery shall create a new DeviceRegistrationState query by calling the createQuery in the registrationStatusManager.] */ return RegistrationStatusManager.CreateEnrollmentGroupQuery( - _provisioningConnectionString, + _hostName, + GetHeaderProvider(), querySpecification, new HttpTransportSettings(), CancellationToken.None, @@ -1297,7 +1420,8 @@ public Query CreateEnrollmentGroupRegistrationStateQuery( { /* SRS_PROVISIONING_SERVICE_CLIENT_21_028: [The createEnrollmentGroupRegistrationStateQuery shall create a new DeviceRegistrationState query by calling the createQuery in the registrationStatusManager.] */ return RegistrationStatusManager.CreateEnrollmentGroupQuery( - _provisioningConnectionString, + _hostName, + GetHeaderProvider(), querySpecification, httpTransportSettings, CancellationToken.None, @@ -1333,7 +1457,8 @@ public Query CreateEnrollmentGroupRegistrationStateQuery( { /* SRS_PROVISIONING_SERVICE_CLIENT_21_028: [The createEnrollmentGroupRegistrationStateQuery shall create a new DeviceRegistrationState query by calling the createQuery in the registrationStatusManager.] */ return RegistrationStatusManager.CreateEnrollmentGroupQuery( - _provisioningConnectionString, + _hostName, + GetHeaderProvider(), querySpecification, new HttpTransportSettings(), cancellationToken, @@ -1368,5 +1493,12 @@ public Task GetEnrollmentGroupAttestationAsync(string enro { return EnrollmentGroupManager.GetEnrollmentAttestationAsync(_contractApiHttp, enrollmentGroupId, cancellationToken); } + + internal IAuthorizationHeaderProvider GetHeaderProvider() + { + return _provisioningConnectionString + ?? (IAuthorizationHeaderProvider) _provisioningTokenCredential + ?? _provisioningSasCredential; + } } } diff --git a/provisioning/service/src/Query.cs b/provisioning/service/src/Query.cs index 47e6d03b60..b548845891 100644 --- a/provisioning/service/src/Query.cs +++ b/provisioning/service/src/Query.cs @@ -80,50 +80,86 @@ internal Query( int pageSize, CancellationToken cancellationToken) { - /* SRS_QUERY_21_001: [The constructor shall throw ArgumentNullException if the provided serviceConnectionString is null.] */ if (serviceConnectionString == null) { throw new ArgumentNullException(nameof(serviceConnectionString)); } - /* SRS_QUERY_21_002: [The constructor shall throw ArgumentException if the provided serviceName is null or empty.] */ if (string.IsNullOrWhiteSpace(serviceName ?? throw new ArgumentNullException(nameof(serviceName)))) { throw new ArgumentException($"{nameof(serviceName)} cannot be an empty string"); } - /* SRS_QUERY_21_003: [The constructor shall throw ArgumentException if the provided querySpecification is null.] */ if (querySpecification == null) { throw new ArgumentNullException(nameof(querySpecification)); } - /* SRS_QUERY_21_004: [The constructor shall throw ArgumentException if the provided pageSize is negative.] */ if (pageSize < 0) { throw new ArgumentException($"{nameof(pageSize)} cannot be negative."); } // TODO: Refactor ContractApiHttp being created again - /* SRS_QUERY_21_005: [The constructor shall create and store a `contractApiHttp` using the provided Service Connection String.] */ _contractApiHttp = new ContractApiHttp( serviceConnectionString.HttpsEndpoint, serviceConnectionString, httpTransportSettings); - /* SRS_QUERY_21_006: [The constructor shall store the provided `pageSize`, and `cancelationToken`.] */ PageSize = pageSize; _cancellationToken = cancellationToken; - /* SRS_QUERY_21_007: [The constructor shall create and store a JSON from the provided querySpecification.] */ _querySpecificationJson = JsonConvert.SerializeObject(querySpecification); - /* SRS_QUERY_21_008: [The constructor shall create and store a queryPath adding `/query` to the provided `targetPath`.] */ _queryPath = GetQueryUri(serviceName); - /* SRS_QUERY_21_009: [The constructor shall set continuationToken and current as null.] */ ContinuationToken = null; - /* SRS_QUERY_21_010: [The constructor shall set hasNext as true.] */ + _hasNext = true; + } + + internal Query( + string hostName, + IAuthorizationHeaderProvider headerProvider, + string serviceName, + QuerySpecification querySpecification, + HttpTransportSettings httpTransportSettings, + int pageSize, + CancellationToken cancellationToken) + { + if (hostName == null) + { + throw new ArgumentNullException(nameof(hostName)); + } + + if (string.IsNullOrWhiteSpace(serviceName ?? throw new ArgumentNullException(nameof(serviceName)))) + { + throw new ArgumentException($"{nameof(serviceName)} cannot be an empty string"); + } + + if (querySpecification == null) + { + throw new ArgumentNullException(nameof(querySpecification)); + } + + if (pageSize < 0) + { + throw new ArgumentException($"{nameof(pageSize)} cannot be negative."); + } + + // TODO: Refactor ContractApiHttp being created again + _contractApiHttp = new ContractApiHttp( + new UriBuilder("https", hostName).Uri, + headerProvider, httpTransportSettings); + + PageSize = pageSize; + _cancellationToken = cancellationToken; + + _querySpecificationJson = JsonConvert.SerializeObject(querySpecification); + + _queryPath = GetQueryUri(serviceName); + + ContinuationToken = null; + _hasNext = true; } @@ -160,17 +196,14 @@ public bool HasNext() /// if the query does no have more pages to return. public async Task NextAsync(string continuationToken) { - /* SRS_QUERY_21_011: [The next shall throw IndexOutOfRangeException if the provided continuationToken is null or empty.] */ if (string.IsNullOrWhiteSpace(continuationToken)) { throw new IndexOutOfRangeException($"There is no {nameof(continuationToken)} to get pending elements."); } - /* SRS_QUERY_21_012: [The next shall store the provided continuationToken.] */ ContinuationToken = continuationToken; _hasNext = true; - /* SRS_QUERY_21_013: [The next shall return the next page of results by calling the next().] */ return await NextAsync().ConfigureAwait(false); } @@ -181,25 +214,21 @@ public async Task NextAsync(string continuationToken) /// if the query does no have more pages to return. public async Task NextAsync() { - /* SRS_QUERY_21_014: [The next shall throw IndexOutOfRangeException if the hasNext is false.] */ if (!_hasNext) { throw new IndexOutOfRangeException("There are no more pending elements"); } - /* SRS_QUERY_21_015: [If the pageSize is not 0, the next shall send the HTTP request with `x-ms-max-item-count=[pageSize]` in the header.] */ IDictionary headerParameters = new Dictionary(); if (PageSize != 0) { headerParameters.Add(PageSizeHeaderKey, PageSize.ToString(CultureInfo.InvariantCulture)); } - /* SRS_QUERY_21_016: [If the continuationToken is not null or empty, the next shall send the HTTP request with `x-ms-continuation=[continuationToken]` in the header.] */ if (!string.IsNullOrWhiteSpace(ContinuationToken)) { headerParameters.Add(ContinuationTokenHeaderKey, ContinuationToken); } - /* SRS_QUERY_21_017: [The next shall send a HTTP request with a HTTP verb `POST`.] */ ContractApiResponse httpResponse = await _contractApiHttp.RequestAsync( HttpMethod.Post, _queryPath, @@ -208,12 +237,10 @@ public async Task NextAsync() null, _cancellationToken).ConfigureAwait(false); - /* SRS_QUERY_21_018: [The next shall create and return a new instance of the QueryResult using the `x-ms-item-type` as type, `x-ms-continuation` as the next continuationToken, and the message body.] */ httpResponse.Fields.TryGetValue(ItemTypeHeaderKey, out string type); httpResponse.Fields.TryGetValue(ContinuationTokenHeaderKey, out string continuationToken); ContinuationToken = continuationToken; - /* SRS_QUERY_21_017: [The next shall set hasNext as true if the continuationToken is not null, or false if it is null.] */ _hasNext = (ContinuationToken != null); var result = new QueryResult(type, httpResponse.Body, ContinuationToken); From a243d2b82d7c547d88898d993badb09b01ab9c6b Mon Sep 17 00:00:00 2001 From: dylanbulfinMS <95251881+dylanbulfinMS@users.noreply.github.com> Date: Tue, 11 Jan 2022 12:43:03 -0500 Subject: [PATCH 2/6] Basic test layout --- .../config/TestConfiguration.Provisioning.cs | 102 +++++++++++++++++- .../ProvisioningServiceClientE2ETests.cs | 40 +++++++ 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/e2e/test/config/TestConfiguration.Provisioning.cs b/e2e/test/config/TestConfiguration.Provisioning.cs index a65f27e557..e720052664 100644 --- a/e2e/test/config/TestConfiguration.Provisioning.cs +++ b/e2e/test/config/TestConfiguration.Provisioning.cs @@ -1,8 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Globalization; +using System.Net; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using System.Text; +using Azure.Identity; namespace Microsoft.Azure.Devices.E2ETests { @@ -14,6 +19,30 @@ public static partial class Provisioning public static string ConnectionString => GetValue("PROVISIONING_CONNECTION_STRING"); + public static string GetProvisioningHostName() + { + var connectionString = new ConnectionStringParser(ConnectionString); + return connectionString.ProvisioningHostName; + } + + public static ClientSecretCredential GetClientSecretCredential() + { + return new ClientSecretCredential( + GetValue("MSFT_TENANT_ID"), + GetValue("PROVISIONING_CLIENT_ID"), + GetValue("PROVISIONING_CLIENT_SECRET")); + } + + public static string GetProvisioningSharedAccessSignature(TimeSpan timeToLive) + { + var connectionString = new ConnectionStringParser(ConnectionString); + return GenerateSasToken( + connectionString.ProvisioningHostName, + connectionString.SharedAccessKey, + timeToLive, + connectionString.SharedAccessKeyName); + } + public static string GlobalDeviceEndpoint => GetValue("DPS_GLOBALDEVICEENDPOINT", "global.azure-devices-provisioning.net"); @@ -38,6 +67,77 @@ public static X509Certificate2Collection GetGroupEnrollmentChain() public static string FarAwayIotHubHostName => GetValue("FAR_AWAY_IOTHUB_HOSTNAME"); public static string CustomAllocationPolicyWebhook => GetValue("CUSTOM_ALLOCATION_POLICY_WEBHOOK"); + + private static string GenerateSasToken(string resourceUri, string sharedAccessKey, TimeSpan timeToLive, string policyName = default) + { + var epochTime = new DateTime(1970, 1, 1); + DateTime expiresOn = DateTime.UtcNow.Add(timeToLive); + TimeSpan secondsFromEpochTime = expiresOn.Subtract(epochTime); + long seconds = Convert.ToInt64(secondsFromEpochTime.TotalSeconds, CultureInfo.InvariantCulture); + string expiry = Convert.ToString(seconds, CultureInfo.InvariantCulture); + + string stringToSign = WebUtility.UrlEncode(resourceUri) + "\n" + expiry; + + using var hmac = new HMACSHA256(Convert.FromBase64String(sharedAccessKey)); + string signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign))); + + // SharedAccessSignature sr=ENCODED(dh://myiothub.azure-devices.net/a/b/c?myvalue1=a)&sig=&se=[&skn=] + string token = string.Format( + CultureInfo.InvariantCulture, + "SharedAccessSignature sr={0}&sig={1}&se={2}", + WebUtility.UrlEncode(resourceUri), + WebUtility.UrlEncode(signature), + expiry); + + if (!string.IsNullOrWhiteSpace(policyName)) + { + token += "&skn=" + policyName; + } + + return token; + } + + public class ConnectionStringParser + { + public ConnectionStringParser(string connectionString) + { + string[] parts = connectionString.Split(';'); + foreach (string part in parts) + { + string[] tv = part.Split('='); + + switch (tv[0].ToUpperInvariant()) + { + case "HOSTNAME": + ProvisioningHostName = part.Substring("HOSTNAME=".Length); + break; + + case "SHAREDACCESSKEY": + SharedAccessKey = part.Substring("SHAREDACCESSKEY=".Length); + break; + + case "DEVICEID": + DeviceID = part.Substring("DEVICEID=".Length); + break; + + case "SHAREDACCESSKEYNAME": + SharedAccessKeyName = part.Substring("SHAREDACCESSKEYNAME=".Length); + break; + + default: + throw new NotSupportedException("Unrecognized tag found in test ConnectionString."); + } + } + } + + public string ProvisioningHostName { get; private set; } + + public string DeviceID { get; private set; } + + public string SharedAccessKey { get; private set; } + + public string SharedAccessKeyName { get; private set; } + } } } } diff --git a/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs b/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs index 1ada9be022..5175471a51 100644 --- a/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs +++ b/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Net; using System.Threading.Tasks; +using Azure; using Microsoft.Azure.Devices.Provisioning.Security.Samples; using Microsoft.Azure.Devices.Provisioning.Service; using Microsoft.Azure.Devices.Shared; @@ -110,6 +111,45 @@ public async Task ProvisioningServiceClient_GetEnrollmentGroupAttestation_Symmet await ProvisioningServiceClient_GetEnrollmentGroupAttestation(AttestationMechanismType.SymmetricKey); } + [LoggedTestMethod] + public async Task ProvisioningServiceClient_TokenCredentialAuth_Success() + { + using var provisioningServiceClient = ProvisioningServiceClient.Create( + TestConfiguration.Provisioning.GetProvisioningHostName(), + TestConfiguration.Provisioning.GetClientSecretCredential()); + + IndividualEnrollment individualEnrollment = await CreateIndividualEnrollment(provisioningServiceClient, AttestationMechanismType.SymmetricKey, null, AllocationPolicy.Static, null, null, null); + + //act + IndividualEnrollment individualEnrollmentResult = await provisioningServiceClient.CreateOrUpdateIndividualEnrollmentAsync(individualEnrollment); + + //assert + Assert.IsNotNull(individualEnrollmentResult); + + //cleanup + await provisioningServiceClient.DeleteIndividualEnrollmentAsync(individualEnrollment.RegistrationId); + } + + [LoggedTestMethod] + public async Task ProvisioningServiceClient_AzureSasCredentialAuth_Success() + { + string signature = TestConfiguration.Provisioning.GetProvisioningSharedAccessSignature(TimeSpan.FromHours(1)); + using var provisioningServiceClient = ProvisioningServiceClient.Create( + TestConfiguration.Provisioning.GetProvisioningHostName(), + new AzureSasCredential(signature)); + + IndividualEnrollment individualEnrollment = await CreateIndividualEnrollment(provisioningServiceClient, AttestationMechanismType.SymmetricKey, null, AllocationPolicy.Static, null, null, null); + + //act + IndividualEnrollment individualEnrollmentResult = await provisioningServiceClient.CreateOrUpdateIndividualEnrollmentAsync(individualEnrollment); + + //assert + Assert.IsNotNull(individualEnrollmentResult); + + //cleanup + await provisioningServiceClient.DeleteIndividualEnrollmentAsync(individualEnrollment.RegistrationId); + } + public async Task ProvisioningServiceClient_GetIndividualEnrollmentAttestation(AttestationMechanismType attestationType) { using var provisioningServiceClient = ProvisioningServiceClient.CreateFromConnectionString(TestConfiguration.Provisioning.ConnectionString); From e4f7ff95d0cdedea5ffe9eafbfd060afb96cd8f3 Mon Sep 17 00:00:00 2001 From: dylanbulfinMS <95251881+dylanbulfinMS@users.noreply.github.com> Date: Fri, 14 Jan 2022 13:03:59 -0500 Subject: [PATCH 3/6] API update --- e2e/test/config/TestConfiguration.Provisioning.cs | 13 +++++++++++-- provisioning/service/src/Contract/SDKUtils.cs | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/e2e/test/config/TestConfiguration.Provisioning.cs b/e2e/test/config/TestConfiguration.Provisioning.cs index e720052664..a21f2b9dea 100644 --- a/e2e/test/config/TestConfiguration.Provisioning.cs +++ b/e2e/test/config/TestConfiguration.Provisioning.cs @@ -7,8 +7,13 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; + +#if !NET451 + using Azure.Identity; +#endif + namespace Microsoft.Azure.Devices.E2ETests { public static partial class TestConfiguration @@ -25,14 +30,18 @@ public static string GetProvisioningHostName() return connectionString.ProvisioningHostName; } +#if !NET451 + public static ClientSecretCredential GetClientSecretCredential() { return new ClientSecretCredential( GetValue("MSFT_TENANT_ID"), - GetValue("PROVISIONING_CLIENT_ID"), - GetValue("PROVISIONING_CLIENT_SECRET")); + GetValue("IOTHUB_CLIENT_ID"), + GetValue("IOTHUB_CLIENT_SECRET")); } +#endif + public static string GetProvisioningSharedAccessSignature(TimeSpan timeToLive) { var connectionString = new ConnectionStringParser(ConnectionString); diff --git a/provisioning/service/src/Contract/SDKUtils.cs b/provisioning/service/src/Contract/SDKUtils.cs index c21488deaf..82a2b31a4e 100644 --- a/provisioning/service/src/Contract/SDKUtils.cs +++ b/provisioning/service/src/Contract/SDKUtils.cs @@ -5,7 +5,7 @@ namespace Microsoft.Azure.Devices.Provisioning.Service { internal class SDKUtils { - private const string ApiVersionProvisioning = "2019-03-31"; + private const string ApiVersionProvisioning = "2021-10-01"; public const string ApiVersionQueryString = CustomHeaderConstants.ApiVersion + "=" + ApiVersionProvisioning; } } From 5062f248056132e631b75b26e203c9267e2d06e4 Mon Sep 17 00:00:00 2001 From: dylanbulfinMS <95251881+dylanbulfinMS@users.noreply.github.com> Date: Tue, 25 Jan 2022 19:18:58 -0500 Subject: [PATCH 4/6] Style updates --- .../config/TestConfiguration.Provisioning.cs | 42 ----------------- .../E2ETestsSetup/e2eTestsSetup.ps1 | 3 +- .../provisioning/ConnectionStringParser.cs | 46 +++++++++++++++++++ .../ProvisioningServiceClientE2ETests.cs | 25 ++++++++-- 4 files changed, 69 insertions(+), 47 deletions(-) create mode 100644 e2e/test/provisioning/ConnectionStringParser.cs diff --git a/e2e/test/config/TestConfiguration.Provisioning.cs b/e2e/test/config/TestConfiguration.Provisioning.cs index a21f2b9dea..2cd65733cf 100644 --- a/e2e/test/config/TestConfiguration.Provisioning.cs +++ b/e2e/test/config/TestConfiguration.Provisioning.cs @@ -105,48 +105,6 @@ private static string GenerateSasToken(string resourceUri, string sharedAccessKe return token; } - - public class ConnectionStringParser - { - public ConnectionStringParser(string connectionString) - { - string[] parts = connectionString.Split(';'); - foreach (string part in parts) - { - string[] tv = part.Split('='); - - switch (tv[0].ToUpperInvariant()) - { - case "HOSTNAME": - ProvisioningHostName = part.Substring("HOSTNAME=".Length); - break; - - case "SHAREDACCESSKEY": - SharedAccessKey = part.Substring("SHAREDACCESSKEY=".Length); - break; - - case "DEVICEID": - DeviceID = part.Substring("DEVICEID=".Length); - break; - - case "SHAREDACCESSKEYNAME": - SharedAccessKeyName = part.Substring("SHAREDACCESSKEYNAME=".Length); - break; - - default: - throw new NotSupportedException("Unrecognized tag found in test ConnectionString."); - } - } - } - - public string ProvisioningHostName { get; private set; } - - public string DeviceID { get; private set; } - - public string SharedAccessKey { get; private set; } - - public string SharedAccessKeyName { get; private set; } - } } } } diff --git a/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1 b/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1 index 0ac2a2e319..6c1c3c0909 100644 --- a/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1 +++ b/e2e/test/prerequisites/E2ETestsSetup/e2eTestsSetup.ps1 @@ -610,13 +610,14 @@ if ($certExists) az iot dps certificate delete -g $ResourceGroup --dps-name $dpsName --name $uploadCertificateName --etag $etag } Write-Host "`nUploading new certificate to DPS." -az iot dps certificate create -g $ResourceGroup --path $rootCertPath --dps-name $dpsName --certificate-name $uploadCertificateName --output none +az iot dps certificate create -g $ResourceGroup --path $rootCertPath --dps-name $dpsName --certificate-name $uploadCertificateName $isVerified = az iot dps certificate show -g $ResourceGroup --dps-name $dpsName --certificate-name $uploadCertificateName --query 'properties.isVerified' --output tsv if ($isVerified -eq 'false') { Write-Host "`nVerifying certificate uploaded to DPS." $etag = az iot dps certificate show -g $ResourceGroup --dps-name $dpsName --certificate-name $uploadCertificateName --query 'etag' + Write-Host "`n az iot dps certificate generate-verification-code -g $ResourceGroup --dps-name $dpsName --certificate-name $uploadCertificateName -e $etag --query 'properties.verificationCode'" $requestedCommonName = az iot dps certificate generate-verification-code -g $ResourceGroup --dps-name $dpsName --certificate-name $uploadCertificateName -e $etag --query 'properties.verificationCode' $verificationCertArgs = @{ "-DnsName" = $requestedCommonName; diff --git a/e2e/test/provisioning/ConnectionStringParser.cs b/e2e/test/provisioning/ConnectionStringParser.cs new file mode 100644 index 0000000000..a2898d6175 --- /dev/null +++ b/e2e/test/provisioning/ConnectionStringParser.cs @@ -0,0 +1,46 @@ +using System; + +namespace Microsoft.Azure.Devices.E2ETests.provisioning +{ + public class ConnectionStringParser + { + public string ProvisioningHostName { get; private set; } + + public string DeviceId { get; private set; } + + public string SharedAccessKey { get; private set; } + + public string SharedAccessKeyName { get; private set; } + + public ConnectionStringParser(string connectionString) + { + string[] parts = connectionString.Split(';'); + foreach (string part in parts) + { + string[] tv = part.Split('='); + + switch (tv[0].ToUpperInvariant()) + { + case "HOSTNAME": + ProvisioningHostName = part.Substring("HOSTNAME=".Length); + break; + + case "SHAREDACCESSKEY": + SharedAccessKey = part.Substring("SHAREDACCESSKEY=".Length); + break; + + case "DEVICEID": + DeviceId = part.Substring("DEVICEID=".Length); + break; + + case "SHAREDACCESSKEYNAME": + SharedAccessKeyName = part.Substring("SHAREDACCESSKEYNAME=".Length); + break; + + default: + throw new NotSupportedException("Unrecognized tag found in test ConnectionString."); + } + } + } + } +} diff --git a/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs b/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs index 5175471a51..f8002109ae 100644 --- a/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs +++ b/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs @@ -6,6 +6,7 @@ using System.Net; using System.Threading.Tasks; using Azure; +using FluentAssertions; using Microsoft.Azure.Devices.Provisioning.Security.Samples; using Microsoft.Azure.Devices.Provisioning.Service; using Microsoft.Azure.Devices.Shared; @@ -118,13 +119,21 @@ public async Task ProvisioningServiceClient_TokenCredentialAuth_Success() TestConfiguration.Provisioning.GetProvisioningHostName(), TestConfiguration.Provisioning.GetClientSecretCredential()); - IndividualEnrollment individualEnrollment = await CreateIndividualEnrollment(provisioningServiceClient, AttestationMechanismType.SymmetricKey, null, AllocationPolicy.Static, null, null, null); + IndividualEnrollment individualEnrollment = await CreateIndividualEnrollment( + provisioningServiceClient, + AttestationMechanismType.SymmetricKey, + null, + AllocationPolicy.Static, + null, + null, + null) + .ConfigureAwait(false); //act IndividualEnrollment individualEnrollmentResult = await provisioningServiceClient.CreateOrUpdateIndividualEnrollmentAsync(individualEnrollment); //assert - Assert.IsNotNull(individualEnrollmentResult); + individualEnrollmentResult.Should().NotBeNull(); //cleanup await provisioningServiceClient.DeleteIndividualEnrollmentAsync(individualEnrollment.RegistrationId); @@ -138,13 +147,21 @@ public async Task ProvisioningServiceClient_AzureSasCredentialAuth_Success() TestConfiguration.Provisioning.GetProvisioningHostName(), new AzureSasCredential(signature)); - IndividualEnrollment individualEnrollment = await CreateIndividualEnrollment(provisioningServiceClient, AttestationMechanismType.SymmetricKey, null, AllocationPolicy.Static, null, null, null); + IndividualEnrollment individualEnrollment = await CreateIndividualEnrollment( + provisioningServiceClient, + AttestationMechanismType.SymmetricKey, + null, + AllocationPolicy.Static, + null, + null, + null) + .ConfigureAwait(false); //act IndividualEnrollment individualEnrollmentResult = await provisioningServiceClient.CreateOrUpdateIndividualEnrollmentAsync(individualEnrollment); //assert - Assert.IsNotNull(individualEnrollmentResult); + individualEnrollmentResult.Should().NotBeNull(); //cleanup await provisioningServiceClient.DeleteIndividualEnrollmentAsync(individualEnrollment.RegistrationId); From 93d3d09a6aace69b085d9303e9a7a6427a994b97 Mon Sep 17 00:00:00 2001 From: dylanbulfinMS <95251881+dylanbulfinMS@users.noreply.github.com> Date: Tue, 25 Jan 2022 19:37:12 -0500 Subject: [PATCH 5/6] Fix namespace issue --- e2e/test/config/TestConfiguration.Provisioning.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e/test/config/TestConfiguration.Provisioning.cs b/e2e/test/config/TestConfiguration.Provisioning.cs index 2cd65733cf..54a46394da 100644 --- a/e2e/test/config/TestConfiguration.Provisioning.cs +++ b/e2e/test/config/TestConfiguration.Provisioning.cs @@ -7,6 +7,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; +using Microsoft.Azure.Devices.E2ETests.provisioning; #if !NET451 From 998604551d63dadcb99cad5ea923a43107223538 Mon Sep 17 00:00:00 2001 From: "David R. Williamson" Date: Mon, 31 Jan 2022 16:51:09 -0800 Subject: [PATCH 6/6] Misc cleanup --- .../config/TestConfiguration.Provisioning.cs | 2 +- .../provisioning/ConnectionStringParser.cs | 31 ++++++++++----- .../ProvisioningServiceClientE2ETests.cs | 38 ++++++++++--------- 3 files changed, 43 insertions(+), 28 deletions(-) diff --git a/e2e/test/config/TestConfiguration.Provisioning.cs b/e2e/test/config/TestConfiguration.Provisioning.cs index 54a46394da..80d8727de7 100644 --- a/e2e/test/config/TestConfiguration.Provisioning.cs +++ b/e2e/test/config/TestConfiguration.Provisioning.cs @@ -7,7 +7,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; -using Microsoft.Azure.Devices.E2ETests.provisioning; +using Microsoft.Azure.Devices.E2ETests.Provisioning; #if !NET451 diff --git a/e2e/test/provisioning/ConnectionStringParser.cs b/e2e/test/provisioning/ConnectionStringParser.cs index a2898d6175..958c78ebb2 100644 --- a/e2e/test/provisioning/ConnectionStringParser.cs +++ b/e2e/test/provisioning/ConnectionStringParser.cs @@ -1,8 +1,8 @@ using System; -namespace Microsoft.Azure.Devices.E2ETests.provisioning +namespace Microsoft.Azure.Devices.E2ETests.Provisioning { - public class ConnectionStringParser + internal class ConnectionStringParser { public string ProvisioningHostName { get; private set; } @@ -14,31 +14,44 @@ public class ConnectionStringParser public ConnectionStringParser(string connectionString) { + if (string.IsNullOrWhiteSpace(connectionString)) + { + throw new ArgumentException(nameof(connectionString), "Parameter cannot be null, empty, or whitespace."); + } + string[] parts = connectionString.Split(';'); + foreach (string part in parts) { - string[] tv = part.Split('='); + int separatorIndex = part.IndexOf('='); + if (separatorIndex < 0) + { + throw new ArgumentException($"Improperly formatted key/value pair: {part}."); + } + + string key = part.Substring(0, separatorIndex); + string value = part.Substring(separatorIndex + 1); - switch (tv[0].ToUpperInvariant()) + switch (key.ToUpperInvariant()) { case "HOSTNAME": - ProvisioningHostName = part.Substring("HOSTNAME=".Length); + ProvisioningHostName = value; break; case "SHAREDACCESSKEY": - SharedAccessKey = part.Substring("SHAREDACCESSKEY=".Length); + SharedAccessKey = value; break; case "DEVICEID": - DeviceId = part.Substring("DEVICEID=".Length); + DeviceId = value; break; case "SHAREDACCESSKEYNAME": - SharedAccessKeyName = part.Substring("SHAREDACCESSKEYNAME=".Length); + SharedAccessKeyName = value; break; default: - throw new NotSupportedException("Unrecognized tag found in test ConnectionString."); + throw new NotSupportedException($"Unrecognized tag found in parameter {nameof(connectionString)}."); } } } diff --git a/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs b/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs index f8002109ae..e847566278 100644 --- a/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs +++ b/e2e/test/provisioning/ProvisioningServiceClientE2ETests.cs @@ -115,55 +115,57 @@ public async Task ProvisioningServiceClient_GetEnrollmentGroupAttestation_Symmet [LoggedTestMethod] public async Task ProvisioningServiceClient_TokenCredentialAuth_Success() { + // arrange using var provisioningServiceClient = ProvisioningServiceClient.Create( TestConfiguration.Provisioning.GetProvisioningHostName(), TestConfiguration.Provisioning.GetClientSecretCredential()); IndividualEnrollment individualEnrollment = await CreateIndividualEnrollment( - provisioningServiceClient, - AttestationMechanismType.SymmetricKey, - null, - AllocationPolicy.Static, - null, - null, + provisioningServiceClient, + AttestationMechanismType.SymmetricKey, + null, + AllocationPolicy.Static, + null, + null, null) .ConfigureAwait(false); - //act + // act IndividualEnrollment individualEnrollmentResult = await provisioningServiceClient.CreateOrUpdateIndividualEnrollmentAsync(individualEnrollment); - //assert + // assert individualEnrollmentResult.Should().NotBeNull(); - //cleanup + // cleanup await provisioningServiceClient.DeleteIndividualEnrollmentAsync(individualEnrollment.RegistrationId); } [LoggedTestMethod] public async Task ProvisioningServiceClient_AzureSasCredentialAuth_Success() { + // arrange string signature = TestConfiguration.Provisioning.GetProvisioningSharedAccessSignature(TimeSpan.FromHours(1)); using var provisioningServiceClient = ProvisioningServiceClient.Create( TestConfiguration.Provisioning.GetProvisioningHostName(), new AzureSasCredential(signature)); IndividualEnrollment individualEnrollment = await CreateIndividualEnrollment( - provisioningServiceClient, - AttestationMechanismType.SymmetricKey, - null, - AllocationPolicy.Static, - null, - null, + provisioningServiceClient, + AttestationMechanismType.SymmetricKey, + null, + AllocationPolicy.Static, + null, + null, null) .ConfigureAwait(false); - //act + // act IndividualEnrollment individualEnrollmentResult = await provisioningServiceClient.CreateOrUpdateIndividualEnrollmentAsync(individualEnrollment); - //assert + // assert individualEnrollmentResult.Should().NotBeNull(); - //cleanup + // cleanup await provisioningServiceClient.DeleteIndividualEnrollmentAsync(individualEnrollment.RegistrationId); }