From 9db34d683a240d145f90adf70f63d532ef304ec3 Mon Sep 17 00:00:00 2001 From: Abhipsa Misra <22563986+abhipsaMisra@users.noreply.github.com> Date: Mon, 15 Jun 2020 17:35:11 -0700 Subject: [PATCH] IoT hub service client authentication via connection string (#12731) * feat(e2e-tests): Add initial setup for E2E tests * feat(iot-service): Add authentication via connection string --- sdk/iot/Azure.Iot.Hub.Service/CodeMaid.config | 4 + .../Azure.Iot.Hub.Service.netstandard2.0.cs | 6 +- .../Authentication/FixedSasTokenProvider.cs | 28 +++++ .../src/Authentication/ISasTokenProvider.cs | 17 +++ .../Authentication/IotHubConnectionString.cs | 62 +++++++++++ .../SasTokenAuthenticationPolicy.cs | 41 +++++++ .../SasTokenProviderWithSharedAccessKey.cs | 81 ++++++++++++++ .../SharedAccessSignatureBuilder.cs | 80 +++++++++++++ .../SharedAccessSignatureConstants.cs | 26 +++++ .../src/Azure.Iot.Hub.Service.csproj | 3 + .../src/IoTHubServiceClient.cs | 105 +++++++++++++++++- .../tests/Azure.Iot.Hub.Service.Tests.csproj | 20 +++- .../tests/E2eTestBase.cs | 46 ++++++++ .../tests/IotHubServiceTestEnvironment.cs | 16 +++ .../tests/TestSettings.cs | 86 ++++++++++++++ .../tests/prerequisites/setup.ps1 | 5 + 16 files changed, 615 insertions(+), 11 deletions(-) create mode 100644 sdk/iot/Azure.Iot.Hub.Service/src/Authentication/FixedSasTokenProvider.cs create mode 100644 sdk/iot/Azure.Iot.Hub.Service/src/Authentication/ISasTokenProvider.cs create mode 100644 sdk/iot/Azure.Iot.Hub.Service/src/Authentication/IotHubConnectionString.cs create mode 100644 sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SasTokenAuthenticationPolicy.cs create mode 100644 sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SasTokenProviderWithSharedAccessKey.cs create mode 100644 sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SharedAccessSignatureBuilder.cs create mode 100644 sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SharedAccessSignatureConstants.cs create mode 100644 sdk/iot/Azure.Iot.Hub.Service/tests/E2eTestBase.cs create mode 100644 sdk/iot/Azure.Iot.Hub.Service/tests/IotHubServiceTestEnvironment.cs create mode 100644 sdk/iot/Azure.Iot.Hub.Service/tests/TestSettings.cs diff --git a/sdk/iot/Azure.Iot.Hub.Service/CodeMaid.config b/sdk/iot/Azure.Iot.Hub.Service/CodeMaid.config index 5382ce6fd8f0..698934e0d176 100644 --- a/sdk/iot/Azure.Iot.Hub.Service/CodeMaid.config +++ b/sdk/iot/Azure.Iot.Hub.Service/CodeMaid.config @@ -36,6 +36,10 @@ False + + False + diff --git a/sdk/iot/Azure.Iot.Hub.Service/api/Azure.Iot.Hub.Service.netstandard2.0.cs b/sdk/iot/Azure.Iot.Hub.Service/api/Azure.Iot.Hub.Service.netstandard2.0.cs index ca6d447ba1f4..e7f94731e4d7 100644 --- a/sdk/iot/Azure.Iot.Hub.Service/api/Azure.Iot.Hub.Service.netstandard2.0.cs +++ b/sdk/iot/Azure.Iot.Hub.Service/api/Azure.Iot.Hub.Service.netstandard2.0.cs @@ -50,8 +50,10 @@ public enum IfMatchPrecondition public partial class IoTHubServiceClient { protected IoTHubServiceClient() { } - public IoTHubServiceClient(System.Uri endpoint) { } - public IoTHubServiceClient(System.Uri endpoint, Azure.Iot.Hub.Service.IoTHubServiceClientOptions options) { } + public IoTHubServiceClient(string connectionString) { } + public IoTHubServiceClient(string connectionString, Azure.Iot.Hub.Service.IoTHubServiceClientOptions options) { } + public IoTHubServiceClient(System.Uri endpoint, Azure.Core.TokenCredential credential) { } + public IoTHubServiceClient(System.Uri endpoint, Azure.Core.TokenCredential credential, Azure.Iot.Hub.Service.IoTHubServiceClientOptions options) { } public Azure.Iot.Hub.Service.DevicesClient Devices { get { throw null; } } public Azure.Iot.Hub.Service.FilesClient Files { get { throw null; } } public Azure.Iot.Hub.Service.JobsClient Jobs { get { throw null; } } diff --git a/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/FixedSasTokenProvider.cs b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/FixedSasTokenProvider.cs new file mode 100644 index 000000000000..6c9fad218911 --- /dev/null +++ b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/FixedSasTokenProvider.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Azure.Iot.Hub.Service.Authentication +{ + /// + /// Implementation of a shared access signature provider with a fixed token. + /// + internal class FixedSasTokenProvider : ISasTokenProvider + { + private readonly string _sharedAccessSignature; + + // Protected constructor, to allow mocking + protected FixedSasTokenProvider() + { + } + + internal FixedSasTokenProvider(string sharedAccessSignature) + { + _sharedAccessSignature = sharedAccessSignature; + } + + public string GetSasToken() + { + return _sharedAccessSignature; + } + } +} diff --git a/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/ISasTokenProvider.cs b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/ISasTokenProvider.cs new file mode 100644 index 000000000000..d6d1bcc174b9 --- /dev/null +++ b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/ISasTokenProvider.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Azure.Iot.Hub.Service.Authentication +{ + /// + /// The token provider interface for shared access signature based authentication. + /// + internal interface ISasTokenProvider + { + /// + /// Retrieve the shared access signature to be used. + /// + /// The shared access signature to be used for authenticating HTTP requests to the service. It is called once per HTTP request. + public string GetSasToken(); + } +} diff --git a/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/IotHubConnectionString.cs b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/IotHubConnectionString.cs new file mode 100644 index 000000000000..258baad4c769 --- /dev/null +++ b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/IotHubConnectionString.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Azure.Core; + +namespace Azure.Iot.Hub.Service.Authentication +{ + /// + /// Implementation for creating the shared access signature token provider. + /// + internal class IotHubConnectionString + { + private const string HostNameIdentifier = "HostName"; + private const string SharedAccessKeyIdentifier = "SharedAccessKey"; + private const string SharedAccessKeyNameIdentifier = "SharedAccessKeyName"; + private const string SharedAccessSignatureIdentifier = "SharedAccessSignature"; + + private readonly ConnectionString _connectionString; + private readonly string _sharedAccessPolicy; + private readonly string _sharedAccessKey; + private readonly string _sharedAccessSignature; + + internal IotHubConnectionString(string connectionString) + { + _connectionString = ConnectionString.Parse(connectionString); + _sharedAccessKey = _connectionString.GetNonRequired(SharedAccessKeyIdentifier); + _sharedAccessPolicy = _connectionString.GetNonRequired(SharedAccessKeyNameIdentifier); + _sharedAccessSignature = _connectionString.GetNonRequired(SharedAccessSignatureIdentifier); + + if (!ValidateInput(_sharedAccessPolicy, _sharedAccessKey, _sharedAccessSignature)) + { + throw new ArgumentException("Specify either both the sharedAccessKey and sharedAccessKeyName, or only sharedAccessSignature"); + } + + HostName = _connectionString.GetRequired(HostNameIdentifier); + } + + internal string HostName { get; } + + internal ISasTokenProvider GetSasTokenProvider() + { + if (_sharedAccessSignature == null) + { + return new SasTokenProviderWithSharedAccessKey(HostName, _sharedAccessPolicy, _sharedAccessKey); + } + return new FixedSasTokenProvider(_sharedAccessSignature); + } + + private static bool ValidateInput(string sharedAccessPolicy, string sharedAccessKey, string sharedAccessSignature) + { + if (sharedAccessSignature == null) + { + return sharedAccessKey != null && sharedAccessPolicy != null; + } + else + { + return sharedAccessKey == null && sharedAccessPolicy == null; + } + } + } +} diff --git a/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SasTokenAuthenticationPolicy.cs b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SasTokenAuthenticationPolicy.cs new file mode 100644 index 000000000000..a657762f4b16 --- /dev/null +++ b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SasTokenAuthenticationPolicy.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Azure.Core; +using Azure.Core.Pipeline; + +namespace Azure.Iot.Hub.Service.Authentication +{ + /// + /// The shared access signature based HTTP pipeline policy. + /// This authentication policy injects the sas token into the HTTP authentication header for each HTTP request made. + /// + internal class SasTokenAuthenticationPolicy : HttpPipelinePolicy + { + private readonly ISasTokenProvider _sasTokenProvider; + + internal SasTokenAuthenticationPolicy(ISasTokenProvider sasTokenProvider) + { + _sasTokenProvider = sasTokenProvider; + } + + public override void Process(HttpMessage message, ReadOnlyMemory pipeline) + { + AddHeaders(message); + ProcessNext(message, pipeline); + } + + public override async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) + { + AddHeaders(message); + await ProcessNextAsync(message, pipeline).ConfigureAwait(false); + } + + private void AddHeaders(HttpMessage message) + { + message.Request.Headers.Add(HttpHeader.Names.Authorization, _sasTokenProvider.GetSasToken()); + } + } +} diff --git a/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SasTokenProviderWithSharedAccessKey.cs b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SasTokenProviderWithSharedAccessKey.cs new file mode 100644 index 000000000000..db6709ef0f76 --- /dev/null +++ b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SasTokenProviderWithSharedAccessKey.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Azure.Iot.Hub.Service.Authentication +{ + /// + /// Implementation of a shared access signature provider with token caching and refresh. + /// + internal class SasTokenProviderWithSharedAccessKey : ISasTokenProvider + { + // The default time to live for a sas token is set to 30 minutes. + private static readonly TimeSpan s_defaultTimeToLive = TimeSpan.FromMinutes(30); + + // Time buffer before expiry when the token should be renewed, expressed as a percentage of the time to live. + // The token will be renewed when it has 15% or less of the sas token's lifespan left. + private const int s_renewalTimeBufferPercentage = 15; + + private readonly object _lock = new object(); + + private readonly string _hostName; + private readonly string _sharedAccessPolicy; + private readonly string _sharedAccessKey; + private readonly TimeSpan _timeToLive; + + private string _cachedSasToken; + private DateTimeOffset _tokenExpiryTime; + + // Protected constructor, to allow mocking + protected SasTokenProviderWithSharedAccessKey() + { + } + + internal SasTokenProviderWithSharedAccessKey(string hostName, string sharedAccessPolicy, string sharedAccessKey, TimeSpan? timeToLive = null) + { + _hostName = hostName; + _sharedAccessPolicy = sharedAccessPolicy; + _sharedAccessKey = sharedAccessKey; + _timeToLive = timeToLive ?? s_defaultTimeToLive; + + _cachedSasToken = null; + } + + string ISasTokenProvider.GetSasToken() + { + lock (_lock) + { + if (IsTokenExpired()) + { + var builder = new SharedAccessSignatureBuilder + { + HostName = _hostName, + SharedAccessPolicy = _sharedAccessPolicy, + SharedAccessKey = _sharedAccessKey, + TimeToLive = _timeToLive, + }; + + _tokenExpiryTime = DateTimeOffset.UtcNow.Add(_timeToLive); + _cachedSasToken = builder.ToSignature(); + } + + return _cachedSasToken; + } + } + + private bool IsTokenExpired() + { + // The token is considered expired if this is the first time it is being accessed (not cached yet) + // or the current time is greater than or equal to the token expiry time, less 15% buffer. + if (_cachedSasToken == null) + { + return true; + } + + var bufferTimeInMilliseconds = (double)s_renewalTimeBufferPercentage / 100 * _timeToLive.TotalMilliseconds; + DateTimeOffset tokenExpiryTimeWithBuffer = _tokenExpiryTime.AddMilliseconds(-bufferTimeInMilliseconds); + return DateTimeOffset.UtcNow.CompareTo(tokenExpiryTimeWithBuffer) >= 0; + } + } +} diff --git a/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SharedAccessSignatureBuilder.cs b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SharedAccessSignatureBuilder.cs new file mode 100644 index 000000000000..2014958c25dc --- /dev/null +++ b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SharedAccessSignatureBuilder.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using System.Security.Cryptography; +using System.Text; + +namespace Azure.Iot.Hub.Service.Authentication +{ + /// + /// Builds the shared access signature based on the access policy passed. + /// + internal class SharedAccessSignatureBuilder + { + internal string SharedAccessPolicy { get; set; } + + internal string SharedAccessKey { get; set; } + + internal string HostName { get; set; } + + internal TimeSpan TimeToLive { get; set; } + + internal string ToSignature() + { + return BuildSignature(SharedAccessPolicy, SharedAccessKey, HostName, TimeToLive); + } + + private static string BuildSignature(string sharedAccessPolicy, string sharedAccessKey, string hostName, TimeSpan timeToLive) + { + string expiresOn = BuildExpiresOn(timeToLive); + string audience = WebUtility.UrlEncode(hostName); + var fields = new List + { + audience, + expiresOn + }; + + // Example string to be signed: + // dh://myiothub.azure-devices.net/a/b/c?myvalue1=a + // + + string signature = Sign(string.Join("\n", fields), sharedAccessKey); + + // Example returned string: + // SharedAccessSignature sr=ENCODED(dh://myiothub.azure-devices.net/a/b/c?myvalue1=a)&sig=&se=[&skn=] + + var buffer = new StringBuilder(); + buffer.Append($"{SharedAccessSignatureConstants.SharedAccessSignature} " + + $"{SharedAccessSignatureConstants.AudienceFieldName}={audience}" + + $"&{SharedAccessSignatureConstants.SignatureFieldName}={WebUtility.UrlEncode(signature)}" + + $"&{SharedAccessSignatureConstants.ExpiryFieldName}={WebUtility.UrlEncode(expiresOn)}"); + + if (!string.IsNullOrWhiteSpace(sharedAccessPolicy)) + { + buffer.Append($"&{SharedAccessSignatureConstants.KeyNameFieldName}={WebUtility.UrlEncode(sharedAccessPolicy)}"); + } + + return buffer.ToString(); + } + + private static string BuildExpiresOn(TimeSpan timeToLive) + { + DateTimeOffset expiresOn = DateTimeOffset.UtcNow.Add(timeToLive); + TimeSpan secondsFromBaseTime = expiresOn.Subtract(SharedAccessSignatureConstants.EpochTime); + long seconds = Convert.ToInt64(secondsFromBaseTime.TotalSeconds, CultureInfo.InvariantCulture); + return Convert.ToString(seconds, CultureInfo.InvariantCulture); + } + + private static string Sign(string requestString, string key) + { + using (var hmac = new HMACSHA256(Convert.FromBase64String(key))) + { + return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(requestString))); + } + } + } +} diff --git a/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SharedAccessSignatureConstants.cs b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SharedAccessSignatureConstants.cs new file mode 100644 index 000000000000..327d6d7d800d --- /dev/null +++ b/sdk/iot/Azure.Iot.Hub.Service/src/Authentication/SharedAccessSignatureConstants.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Azure.Iot.Hub.Service.Authentication +{ + /// + /// The constants used for building the IoT hub service shared access signature token. + /// + internal static class SharedAccessSignatureConstants + { + internal const int MaxKeyNameLength = 256; + internal const int MaxKeyLength = 256; + internal const string SharedAccessSignature = "SharedAccessSignature"; + internal const string AudienceFieldName = "sr"; + internal const string SignatureFieldName = "sig"; + internal const string KeyNameFieldName = "skn"; + internal const string ExpiryFieldName = "se"; + internal const string SignedResourceFullFieldName = SharedAccessSignature + " " + AudienceFieldName; + internal const string KeyValueSeparator = "="; + internal const string PairSeparator = "&"; + internal static readonly DateTime EpochTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + internal static readonly TimeSpan MaxClockSkew = TimeSpan.FromMinutes(5); + } +} diff --git a/sdk/iot/Azure.Iot.Hub.Service/src/Azure.Iot.Hub.Service.csproj b/sdk/iot/Azure.Iot.Hub.Service/src/Azure.Iot.Hub.Service.csproj index 8ea0f5882dff..0c15236760cf 100644 --- a/sdk/iot/Azure.Iot.Hub.Service/src/Azure.Iot.Hub.Service.csproj +++ b/sdk/iot/Azure.Iot.Hub.Service/src/Azure.Iot.Hub.Service.csproj @@ -47,6 +47,9 @@ Shared\Azure.Core + + Shared\Azure.Core + diff --git a/sdk/iot/Azure.Iot.Hub.Service/src/IoTHubServiceClient.cs b/sdk/iot/Azure.Iot.Hub.Service/src/IoTHubServiceClient.cs index 749198543663..ef1e1f52dee1 100644 --- a/sdk/iot/Azure.Iot.Hub.Service/src/IoTHubServiceClient.cs +++ b/sdk/iot/Azure.Iot.Hub.Service/src/IoTHubServiceClient.cs @@ -4,16 +4,18 @@ using System; using Azure.Core; using Azure.Core.Pipeline; +using Azure.Iot.Hub.Service.Authentication; namespace Azure.Iot.Hub.Service { /// - /// The IoT Hub Service Client + /// The IoT Hub Service Client. /// public class IoTHubServiceClient { private readonly HttpPipeline _httpPipeline; private readonly ClientDiagnostics _clientDiagnostics; + private readonly Uri _endpoint; private readonly RegistryManagerRestClient _registryManagerRestClient; private readonly TwinRestClient _twinRestClient; private readonly DeviceMethodRestClient _deviceMethodRestClient; @@ -22,22 +24,27 @@ public class IoTHubServiceClient /// place holder for Devices /// public DevicesClient Devices { get; private set; } + /// /// place holder for Modules /// public ModulesClient Modules { get; private set; } + /// /// place holder for Statistics /// public StatisticsClient Statistics { get; private set; } + /// /// place holder for Messages /// public CloudToDeviceMessagesClient Messages { get; private set; } + /// /// place holder for Files /// public FilesClient Files { get; private set; } + /// /// place holder for Jobs /// @@ -54,24 +61,91 @@ protected IoTHubServiceClient() /// /// Initializes a new instance of the class. /// - public IoTHubServiceClient(Uri endpoint) - : this(endpoint, new IoTHubServiceClientOptions()) + /// + /// The IoT Hub connection string, with either "iothubowner", "service", "registryRead" or "registryReadWrite" policy, as applicable. + /// For more information, see . + /// + public IoTHubServiceClient(string connectionString) + : this(connectionString, new IoTHubServiceClientOptions()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The IoT Hub connection string, with either "iothubowner", "service", "registryRead" or "registryReadWrite" policy, as applicable. + /// For more information, see . + /// + /// + /// Options that allow configuration of requests sent to the IoT Hub service. + /// + public IoTHubServiceClient(string connectionString, IoTHubServiceClientOptions options) + { + Argument.AssertNotNull(options, nameof(options)); + + var iotHubConnectionString = new IotHubConnectionString(connectionString); + ISasTokenProvider sasProvider = iotHubConnectionString.GetSasTokenProvider(); + + _endpoint = BuildEndpointUriFromHostName(iotHubConnectionString.HostName); + + _clientDiagnostics = new ClientDiagnostics(options); + + options.AddPolicy(new SasTokenAuthenticationPolicy(sasProvider), HttpPipelinePosition.PerCall); + _httpPipeline = HttpPipelineBuilder.Build(options); + + _registryManagerRestClient = new RegistryManagerRestClient(_clientDiagnostics, _httpPipeline, _endpoint, options.GetVersionString()); + _twinRestClient = new TwinRestClient(_clientDiagnostics, _httpPipeline, null, options.GetVersionString()); + _deviceMethodRestClient = new DeviceMethodRestClient(_clientDiagnostics, _httpPipeline, _endpoint, options.GetVersionString()); + + Devices = new DevicesClient(_registryManagerRestClient, _twinRestClient, _deviceMethodRestClient); + Modules = new ModulesClient(_registryManagerRestClient, _twinRestClient, _deviceMethodRestClient); + + Statistics = new StatisticsClient(); + Messages = new CloudToDeviceMessagesClient(); + Files = new FilesClient(); + Jobs = new JobsClient(); + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The IoT Hub service instance URI to connect to. + /// + /// + /// The implementation which will be used to request for the authentication token. + /// + public IoTHubServiceClient(Uri endpoint, TokenCredential credential) + : this(endpoint, credential, new IoTHubServiceClientOptions()) { } /// /// Initializes a new instance of the class. /// - public IoTHubServiceClient(Uri endpoint, IoTHubServiceClientOptions options) + /// + /// The IoT Hub service instance URI to connect to. + /// + /// + /// The implementation which will be used to request for the authentication token. + /// + /// + /// Options that allow configuration of requests sent to the IoT Hub service. + /// + public IoTHubServiceClient(Uri endpoint, TokenCredential credential, IoTHubServiceClientOptions options) { Argument.AssertNotNull(options, nameof(options)); + _endpoint = endpoint; _clientDiagnostics = new ClientDiagnostics(options); + + options.AddPolicy(new BearerTokenAuthenticationPolicy(credential, GetAuthorizationScopes(_endpoint)), HttpPipelinePosition.PerCall); _httpPipeline = HttpPipelineBuilder.Build(options); - _registryManagerRestClient = new RegistryManagerRestClient(_clientDiagnostics, _httpPipeline, endpoint, options.GetVersionString()); + _registryManagerRestClient = new RegistryManagerRestClient(_clientDiagnostics, _httpPipeline, _endpoint, options.GetVersionString()); _twinRestClient = new TwinRestClient(_clientDiagnostics, _httpPipeline, null, options.GetVersionString()); - _deviceMethodRestClient = new DeviceMethodRestClient(_clientDiagnostics, _httpPipeline, endpoint, options.GetVersionString()); + _deviceMethodRestClient = new DeviceMethodRestClient(_clientDiagnostics, _httpPipeline, _endpoint, options.GetVersionString()); Devices = new DevicesClient(_registryManagerRestClient, _twinRestClient, _deviceMethodRestClient); Modules = new ModulesClient(_registryManagerRestClient, _twinRestClient, _deviceMethodRestClient); @@ -81,5 +155,24 @@ public IoTHubServiceClient(Uri endpoint, IoTHubServiceClientOptions options) Files = new FilesClient(); Jobs = new JobsClient(); } + + /// + /// Gets the scope for authentication/authorization policy. + /// + /// The IoT Hub service instance Uri. + /// List of scopes for the specified endpoint. + internal static string[] GetAuthorizationScopes(Uri endpoint) + { + Argument.AssertNotNull(endpoint, nameof(endpoint)); + Argument.AssertNotNullOrEmpty(endpoint.AbsoluteUri, nameof(endpoint.AbsoluteUri)); + + // TODO: GetAuthorizationScopes for IoT Hub + return null; + } + + private static Uri BuildEndpointUriFromHostName(string hostName) + { + return new UriBuilder { Scheme = "https", Host = hostName }.Uri; + } } } diff --git a/sdk/iot/Azure.Iot.Hub.Service/tests/Azure.Iot.Hub.Service.Tests.csproj b/sdk/iot/Azure.Iot.Hub.Service/tests/Azure.Iot.Hub.Service.Tests.csproj index 45ad4b09d71f..8e85dd564054 100644 --- a/sdk/iot/Azure.Iot.Hub.Service/tests/Azure.Iot.Hub.Service.Tests.csproj +++ b/sdk/iot/Azure.Iot.Hub.Service/tests/Azure.Iot.Hub.Service.Tests.csproj @@ -6,14 +6,28 @@ - - + + + - + + + + + PreserveNewest + + + + + + + + + diff --git a/sdk/iot/Azure.Iot.Hub.Service/tests/E2eTestBase.cs b/sdk/iot/Azure.Iot.Hub.Service/tests/E2eTestBase.cs new file mode 100644 index 000000000000..dbe4eee71b1d --- /dev/null +++ b/sdk/iot/Azure.Iot.Hub.Service/tests/E2eTestBase.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Net; +using Azure.Core.TestFramework; +using NUnit.Framework; + +namespace Azure.Iot.Hub.Service.Tests +{ + /// + /// This class will initialize all the settings and create and instance of the IoT Hub service client. + /// + [Parallelizable(ParallelScope.Self)] + public abstract class E2eTestBase : RecordedTestBase + { + public E2eTestBase(bool isAsync) + : base(isAsync, TestSettings.Instance.TestMode) + { + } + + public E2eTestBase(bool isAsync, RecordedTestMode testMode) + : base(isAsync, testMode) + { + } + + [SetUp] + public virtual void SetupE2eTestBase() + { + TestDiagnostics = false; + + // TODO: set via client options and pipeline instead + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; + } + + protected IoTHubServiceClient GetClient() + { + return InstrumentClient( + new IoTHubServiceClient(TestSettings.Instance.IotHubConnectionString)); + } + + protected string GetRandom() + { + return Recording.GenerateId(); + } + } +} diff --git a/sdk/iot/Azure.Iot.Hub.Service/tests/IotHubServiceTestEnvironment.cs b/sdk/iot/Azure.Iot.Hub.Service/tests/IotHubServiceTestEnvironment.cs new file mode 100644 index 000000000000..c3a37af7d9e9 --- /dev/null +++ b/sdk/iot/Azure.Iot.Hub.Service/tests/IotHubServiceTestEnvironment.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Core.TestFramework; + +namespace Azure.Iot.Hub.Service.Tests +{ + // This class contains the configurations required to be set to run tests against the CI pipeline. + public class IotHubServiceTestEnvironment : TestEnvironment + { + public IotHubServiceTestEnvironment() + : base("iot") + { + } + } +} diff --git a/sdk/iot/Azure.Iot.Hub.Service/tests/TestSettings.cs b/sdk/iot/Azure.Iot.Hub.Service/tests/TestSettings.cs new file mode 100644 index 000000000000..753d80a04bf0 --- /dev/null +++ b/sdk/iot/Azure.Iot.Hub.Service/tests/TestSettings.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Reflection; +using Azure.Core.TestFramework; +using Microsoft.Extensions.Configuration; + +namespace Azure.Iot.Hub.Service.Tests +{ + /// + /// These are the settings that will be used by the end-to-end tests tests. + /// The json files configured in the config will load the settings specific to a user. + /// + public class TestSettings + { + public const string IotHubServiceEnvironmentVariablesPrefix = "IOT"; + + // These environment variables are required to be set to run tests against the CI pipeline. + // If these environment variables exist in the environment, their values will replace (supersede) config.json values. + private static readonly string s_iotHubConnectionString = $"{IotHubServiceEnvironmentVariablesPrefix}_CONNECTION_STRING"; + + private static readonly string s_iotHubServiceTestMode = $"AZURE_IOT_TEST_MODE"; + + public static TestSettings Instance { get; private set; } + + public RecordedTestMode TestMode { get; set; } + + /// + /// The working directory of the tests. + /// + public string WorkingDirectory { get; private set; } + + /// + /// The IoT Hub instance connection string. + /// + public string IotHubConnectionString { get; set; } + + /// + /// The IoT Hub instance hostName. + /// + public string IotHubHostName { get; set; } + + static TestSettings() + { + if (Instance != null) + { + return; + } + + string codeBase = Assembly.GetExecutingAssembly().CodeBase; + var uri = new UriBuilder(codeBase); + string path = Uri.UnescapeDataString(uri.Path); + string workingDirectory = Path.GetDirectoryName(path); + + string userName = Environment.UserName; + + // Initialize the settings related to IoT Hub instance and auth + var testSettingsConfigBuilder = new ConfigurationBuilder(); + + string testSettingsCommonPath = Path.Combine(workingDirectory, "config", "common.config.json"); + testSettingsConfigBuilder.AddJsonFile(testSettingsCommonPath); + + string testSettingsUserPath = Path.Combine(workingDirectory, "config", $"{userName}.config.json"); + if (File.Exists(testSettingsUserPath)) + { + testSettingsConfigBuilder.AddJsonFile(testSettingsUserPath); + } + + IConfiguration config = testSettingsConfigBuilder.Build(); + + // This will set the values from the above config files into the TestSettings Instance. + Instance = config.Get(); + Instance.WorkingDirectory = workingDirectory; + + // We will override settings if they can be found in the environment variables. + OverrideFromEnvVariables(); + } + + // These environment variables are required to be set to run tests against the CI pipeline. + private static void OverrideFromEnvVariables() + { + } + } +} diff --git a/sdk/iot/Azure.Iot.Hub.Service/tests/prerequisites/setup.ps1 b/sdk/iot/Azure.Iot.Hub.Service/tests/prerequisites/setup.ps1 index e331d2ad6fd2..30340a77e0af 100644 --- a/sdk/iot/Azure.Iot.Hub.Service/tests/prerequisites/setup.ps1 +++ b/sdk/iot/Azure.Iot.Hub.Service/tests/prerequisites/setup.ps1 @@ -115,8 +115,13 @@ Write-Host("Writing user config file - $fileName`n") $appSecretJsonEscaped = ConvertTo-Json $appSecret $config = @" { +<<<<<<< HEAD "IotHubConnectionString": "$iotHubConnectionString", "IotHubHostName": "$iotHubHostName", +======= + "IotHubHostName": "$iotHubHostName", + "IotHubConnectionString": "$iotHubConnectionString", +>>>>>>> 0223e524ec... feat(e2e-tests): Add initial setup for E2E tests "ApplicationId": "$appId", "ClientSecret": $appSecretJsonEscaped, "TestMode": "Live"