diff --git a/DiagnosticsExtension/Controllers/ConnectionStringValidationController.cs b/DiagnosticsExtension/Controllers/ConnectionStringValidationController.cs index 29623e8c..69aae284 100644 --- a/DiagnosticsExtension/Controllers/ConnectionStringValidationController.cs +++ b/DiagnosticsExtension/Controllers/ConnectionStringValidationController.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------- +// ----------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. @@ -37,6 +37,9 @@ public ConnectionStringValidationController() new MySqlValidator(), new KeyVaultValidator(), new StorageValidator(), + new BlobStorageValidator(), + new QueueStorageValidator(), + new FileShareStorageValidator(), new ServiceBusValidator(), new EventHubsValidator(), new HttpValidator() @@ -72,25 +75,31 @@ public async Task Validate([FromBody] ConnectionStringReque { return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Type is not specified in the request body"); } - var result = await Validate(requestBody.ConnectionString, requestBody.Type); return result; } + /// + /// This method is used to validate connection information via appsetting. + /// + /// This should be the configured appsetting key/connection property of the function. + /// This should be the type of Azure service being connected. + /// This is valid only for servicebus and eventhub. Need not be passed for other azure services. + /// ConnectionStringValidationResult [HttpGet] - [Route("validateappsetting")] - public async Task ValidateAppSetting(string appSettingName, string type) + [Route("validateappsettingforfunctionapp")] + public async Task ValidateAppSettingForFunctionApp(string appSettingName, string type, string entityName = null) { - var envDict = Environment.GetEnvironmentVariables(); - if (envDict.Contains(appSettingName)) + bool success = Enum.TryParse(type, out ConnectionStringType csType); + if (success && typeValidatorMap.ContainsKey(csType)) { - var connectionString = (string)envDict[appSettingName]; - return await Validate(connectionString, type); + var result = await typeValidatorMap[csType].ValidateViaAppsettingAsync(appSettingName, entityName); + return Request.CreateResponse(HttpStatusCode.OK, result); } else { - return Request.CreateErrorResponse(HttpStatusCode.NotFound, $"AppSetting {appSettingName} not found"); + return Request.CreateErrorResponse(HttpStatusCode.BadRequest, $"Type '{type}' is not supported"); } } } -} \ No newline at end of file +} diff --git a/DiagnosticsExtension/DiagnosticsExtension.csproj b/DiagnosticsExtension/DiagnosticsExtension.csproj index 56e25f7a..5bb3c96c 100644 --- a/DiagnosticsExtension/DiagnosticsExtension.csproj +++ b/DiagnosticsExtension/DiagnosticsExtension.csproj @@ -55,6 +55,36 @@ ..\packages\Antlr.3.5.0.2\lib\Antlr3.Runtime.dll + + ..\packages\Azure.Core.1.22.0\lib\net461\Azure.Core.dll + + + ..\packages\Azure.Core.Amqp.1.2.0\lib\netstandard2.0\Azure.Core.Amqp.dll + + + ..\packages\Azure.Identity.1.4.0\lib\netstandard2.0\Azure.Identity.dll + + + ..\packages\Azure.Messaging.EventHubs.5.6.2\lib\netstandard2.0\Azure.Messaging.EventHubs.dll + + + ..\packages\Azure.Messaging.EventHubs.Processor.5.6.2\lib\netstandard2.0\Azure.Messaging.EventHubs.Processor.dll + + + ..\packages\Azure.Messaging.ServiceBus.7.6.0\lib\netstandard2.0\Azure.Messaging.ServiceBus.dll + + + ..\packages\Azure.Storage.Blobs.12.10.0\lib\netstandard2.0\Azure.Storage.Blobs.dll + + + ..\packages\Azure.Storage.Common.12.9.0\lib\netstandard2.0\Azure.Storage.Common.dll + + + ..\packages\Azure.Storage.Files.Shares.12.8.0\lib\netstandard2.0\Azure.Storage.Files.Shares.dll + + + ..\packages\Azure.Storage.Queues.12.8.0\lib\netstandard2.0\Azure.Storage.Queues.dll + ..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll @@ -62,7 +92,7 @@ ..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll - ..\packages\Microsoft.Azure.Amqp.2.4.9\lib\net45\Microsoft.Azure.Amqp.dll + ..\packages\Microsoft.Azure.Amqp.2.5.10\lib\net45\Microsoft.Azure.Amqp.dll ..\packages\Microsoft.Azure.EventHubs.3.0.0\lib\net461\Microsoft.Azure.EventHubs.dll @@ -79,6 +109,9 @@ ..\packages\Microsoft.Azure.Services.AppAuthentication.1.0.3\lib\net452\Microsoft.Azure.Services.AppAuthentication.dll + + ..\packages\Microsoft.Bcl.AsyncInterfaces.1.1.1\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll + ..\packages\Microsoft.Data.Edm.5.8.4\lib\net40\Microsoft.Data.Edm.dll @@ -89,17 +122,23 @@ ..\packages\Microsoft.Data.Services.Client.5.8.4\lib\net40\Microsoft.Data.Services.Client.dll + + ..\packages\Microsoft.Identity.Client.4.30.1\lib\net461\Microsoft.Identity.Client.dll + + + ..\packages\Microsoft.Identity.Client.Extensions.Msal.2.18.4\lib\net45\Microsoft.Identity.Client.Extensions.Msal.dll + ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.4.5.0\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll - - ..\packages\Microsoft.IdentityModel.JsonWebTokens.5.4.0\lib\net461\Microsoft.IdentityModel.JsonWebTokens.dll + + ..\packages\Microsoft.IdentityModel.JsonWebTokens.6.10.0\lib\net472\Microsoft.IdentityModel.JsonWebTokens.dll - - ..\packages\Microsoft.IdentityModel.Logging.5.4.0\lib\net461\Microsoft.IdentityModel.Logging.dll + + ..\packages\Microsoft.IdentityModel.Logging.6.10.0\lib\net472\Microsoft.IdentityModel.Logging.dll - - ..\packages\Microsoft.IdentityModel.Tokens.5.4.0\lib\net461\Microsoft.IdentityModel.Tokens.dll + + ..\packages\Microsoft.IdentityModel.Tokens.6.10.0\lib\net472\Microsoft.IdentityModel.Tokens.dll ..\packages\CommonServiceLocator.1.3\lib\portable-net4+sl5+netcore45+wpa81+wp8\Microsoft.Practices.ServiceLocation.dll @@ -127,6 +166,9 @@ ..\packages\Swashbuckle.Core.5.6.0\lib\net40\Swashbuckle.Core.dll + + ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + @@ -134,21 +176,28 @@ ..\packages\System.Data.SqlClient.4.8.2\lib\net461\System.Data.SqlClient.dll - - ..\packages\System.Diagnostics.DiagnosticSource.4.5.1\lib\net46\System.Diagnostics.DiagnosticSource.dll + + ..\packages\System.Diagnostics.DiagnosticSource.4.6.0\lib\net46\System.Diagnostics.DiagnosticSource.dll - - ..\packages\System.IdentityModel.Tokens.Jwt.5.4.0\lib\net461\System.IdentityModel.Tokens.Jwt.dll + + ..\packages\System.IdentityModel.Tokens.Jwt.6.10.0\lib\net472\System.IdentityModel.Tokens.Jwt.dll ..\packages\System.IO.4.3.0\lib\net462\System.IO.dll True - - ..\packages\System.Net.Http.4.3.3\lib\net46\System.Net.Http.dll + + ..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll + + + ..\packages\System.Memory.Data.1.0.2\lib\net461\System.Memory.Data.dll + + + ..\packages\System.Net.Http.4.3.4\lib\net46\System.Net.Http.dll + True True @@ -168,21 +217,31 @@ ..\packages\System.Net.WebSockets.Client.4.0.2\lib\net46\System.Net.WebSockets.Client.dll True + + + ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + - ..\packages\System.Reflection.TypeExtensions.4.5.0\lib\net461\System.Reflection.TypeExtensions.dll + ..\packages\System.Reflection.TypeExtensions.4.5.1\lib\net461\System.Reflection.TypeExtensions.dll ..\packages\System.Runtime.4.3.0\lib\net462\System.Runtime.dll True - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.7.1\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + ..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll + True + True ..\packages\System.Runtime.Serialization.Primitives.4.1.1\lib\net46\System.Runtime.Serialization.Primitives.dll True + ..\packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net463\System.Security.Cryptography.Algorithms.dll True @@ -195,6 +254,9 @@ ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll True + + ..\packages\System.Security.Cryptography.ProtectedData.4.5.0\lib\net461\System.Security.Cryptography.ProtectedData.dll + ..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net461\System.Security.Cryptography.X509Certificates.dll True @@ -202,8 +264,17 @@ ..\packages\System.Spatial.5.8.4\lib\net40\System.Spatial.dll - - ..\packages\System.Threading.Tasks.Extensions.4.5.1\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll + + ..\packages\System.Text.Encodings.Web.4.7.2\lib\net461\System.Text.Encodings.Web.dll + + + ..\packages\System.Text.Json.4.7.2\lib\net461\System.Text.Json.dll + + + ..\packages\System.Threading.Channels.4.6.0\lib\netstandard2.0\System.Threading.Channels.dll + + + ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll ..\packages\System.ValueTuple.4.5.0\lib\netstandard1.0\System.ValueTuple.dll @@ -303,16 +374,25 @@ + + + + + + + + + - + diff --git a/DiagnosticsExtension/Models/ConnectionStringValidator/BlobStorageValidator.cs b/DiagnosticsExtension/Models/ConnectionStringValidator/BlobStorageValidator.cs new file mode 100644 index 00000000..191552e7 --- /dev/null +++ b/DiagnosticsExtension/Models/ConnectionStringValidator/BlobStorageValidator.cs @@ -0,0 +1,133 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. +// +// ----------------------------------------------------------------------- + +using DiagnosticsExtension.Controllers; +using DiagnosticsExtension.Models.ConnectionStringValidator.Exceptions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Azure.Storage.Blobs; +using Azure.Storage.Queues; +using Azure.Storage.Blobs.Models; +using Microsoft.WindowsAzure.Storage; +using Azure.Core; +using Azure.Identity; + +namespace DiagnosticsExtension.Models.ConnectionStringValidator +{ + public class BlobStorageValidator : IConnectionStringValidator + { + public string ProviderName => "Microsoft.WindowsAzure.Storage"; + public ConnectionStringType Type => ConnectionStringType.BlobStorageAccount; + public async Task ValidateViaAppsettingAsync(string appSettingName, string entityName) + { + ConnectionStringValidationResult response = new ConnectionStringValidationResult(Type); + bool isManagedIdentityConnection = false; + try + { + var envDict = Environment.GetEnvironmentVariables(); + string appSettingClientIdValue, appSettingClientCredValue = null; + BlobServiceClient client = null; + if (envDict.Contains(appSettingName)) + { + // Connection String + try + { + string connectionString = Environment.GetEnvironmentVariable(appSettingName); + client = new BlobServiceClient(connectionString); + } + catch (ArgumentNullException e) + { + throw new EmptyConnectionStringException(e.Message, e); + } + catch (Exception e) + { + throw new MalformedConnectionStringException(e.Message, e); + } + } + else + { + // Managed Identity + isManagedIdentityConnection = true; + string serviceUriString = ManagedIdentityConnectionResponseUtility.ResolveManagedIdentityCommonProperty(appSettingName, ConnectionStringValidationResult.ManagedIdentityCommonProperty.blobServiceUri); + if (string.IsNullOrEmpty(serviceUriString)) + { + serviceUriString = ManagedIdentityConnectionResponseUtility.ResolveManagedIdentityCommonProperty(appSettingName, ConnectionStringValidationResult.ManagedIdentityCommonProperty.serviceUri); + } + if (!string.IsNullOrEmpty(serviceUriString)) + { + string clientIdAppSettingKey = Environment.GetEnvironmentVariables().Keys.Cast().Where(k => k.StartsWith(appSettingName) && k.ToLower().EndsWith("clientid")).FirstOrDefault(); + appSettingClientIdValue = ManagedIdentityConnectionResponseUtility.ResolveManagedIdentityCommonProperty(appSettingName, ConnectionStringValidationResult.ManagedIdentityCommonProperty.clientId); + appSettingClientCredValue = ManagedIdentityConnectionResponseUtility.ResolveManagedIdentityCommonProperty(appSettingName, ConnectionStringValidationResult.ManagedIdentityCommonProperty.credential); + if (appSettingClientCredValue != null && appSettingClientCredValue != Constants.ValidCredentialValue) + { + throw new ManagedIdentityException(String.Format(Constants.ManagedIdentityCredentialInvalidSummary, appSettingName)); + } + Uri serviceUri = new Uri(serviceUriString); + // If the user has configured __credential with "managedidentity" and set an app setting for __clientId (even if its empty) we assume their intent is to use a user assigned managed identity + if (appSettingClientCredValue != null && clientIdAppSettingKey != null) + { + if (string.IsNullOrEmpty(appSettingClientIdValue)) + { + throw new ManagedIdentityException(String.Format(Constants.ManagedIdentityClientIdEmptySummary, clientIdAppSettingKey), + String.Format(Constants.ManagedIdentityClientIdEmptyDetails, appSettingName)); + } + response.IdentityType = Constants.User; + client = new BlobServiceClient(serviceUri, ManagedIdentityCredentialTokenValidator.GetValidatedCredential(appSettingClientIdValue,appSettingName)); + } + else + { + // Creating client using System assigned managed identity + response.IdentityType = Constants.System; + client = new BlobServiceClient(serviceUri, new Azure.Identity.ManagedIdentityCredential()); + } + } + else + { + string serviceuriAppSettingName = Environment.GetEnvironmentVariables().Keys.Cast().Where(k => k.StartsWith(appSettingName) && k.ToLower().EndsWith("serviceuri")).FirstOrDefault(); + if (serviceuriAppSettingName == null) + { + throw new ManagedIdentityException(Constants.BlobServiceUriMissingSummary); + } + throw new ManagedIdentityException(String.Format(Constants.BlobServiceUriEmptySummary, serviceuriAppSettingName)); + + } + } + var resultSegment = + client.GetBlobContainers(BlobContainerTraits.Metadata, null, default) + .AsPages(default, 10); + //need to read at least one result item to confirm authorization check for connection + resultSegment.Single(); + + response.Status = ConnectionStringValidationResult.ResultStatus.Success; + } + catch (Exception e) + { + if (isManagedIdentityConnection) + { + ManagedIdentityConnectionResponseUtility.EvaluateResponseStatus(e, Type, ref response, appSettingName); + } + else + { + ConnectionStringResponseUtility.EvaluateResponseStatus(e, Type, ref response, appSettingName); + } + } + + return response; + } + public async Task ValidateAsync(string connStr, string clientId = null) + { + throw new NotImplementedException(); + } + public async Task IsValidAsync(string connStr) + { + throw new NotImplementedException(); + } + + } +} diff --git a/DiagnosticsExtension/Models/ConnectionStringValidator/ConnectionStringResponseUtility.cs b/DiagnosticsExtension/Models/ConnectionStringValidator/ConnectionStringResponseUtility.cs new file mode 100644 index 00000000..6bf4914f --- /dev/null +++ b/DiagnosticsExtension/Models/ConnectionStringValidator/ConnectionStringResponseUtility.cs @@ -0,0 +1,214 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. +// +// ----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using Azure; +using Azure.Identity; +using Microsoft.WindowsAzure.Storage; +using DiagnosticsExtension.Models.ConnectionStringValidator.Exceptions; + +namespace DiagnosticsExtension.Models.ConnectionStringValidator +{ + public static class ConnectionStringResponseUtility + { + public static void EvaluateResponseStatus(Exception e, ConnectionStringType type, ref ConnectionStringValidationResult response, string appSettingName = "") + { + // Check if the value is a key vault reference that failed to resolve to the connection string by platform + if (ConnectionStringResponseUtility.IsKeyVaultReference(Environment.GetEnvironmentVariable(appSettingName))) + { + response.Status = ConnectionStringValidationResult.ResultStatus.KeyVaultReferenceResolutionFailed; + response.Summary = String.Format(Constants.KeyVaultReferenceResolutionFailedSummary, appSettingName); + } + else if (e is MalformedConnectionStringException) + { + response.Status = ConnectionStringValidationResult.ResultStatus.MalformedConnectionString; + response.Summary = String.Format(Constants.MalformedConnectionStringDetails, appSettingName); + response.Details = Constants.GenericDetailsMessage; + response.Exception = e; + } + else if (e is EmptyConnectionStringException) + { + response.Status = ConnectionStringValidationResult.ResultStatus.EmptyConnectionString; + response.Summary = "The app setting " + appSettingName + " was not found or is set to a blank value"; + response.Exception = e; + } + else if (e is UnauthorizedAccessException && e.Message.Contains("unauthorized") || e.Message.Contains("Unauthorized") || e.Message.Contains("request is not authorized")) + { + response.Status = ConnectionStringValidationResult.ResultStatus.AuthFailure; + response.Summary = String.Format(Constants.AuthFailureSummary, appSettingName) + " " + GetRelevantAuthFailureDocs(type); + response.Details = Constants.GenericDetailsMessage; + response.Exception = e; + } + else if (e.InnerException != null && + e.InnerException.Message.Contains("The remote name could not be resolved")) + { + response.Status = ConnectionStringValidationResult.ResultStatus.DnsLookupFailed; + response.Summary = Constants.DnsLookupFailed; + response.Exception = e; + } + else if (e.InnerException != null && e.InnerException.InnerException != null && + e.InnerException.InnerException.Message.Contains("The remote name could not be resolved")) + { + response.Status = ConnectionStringValidationResult.ResultStatus.DnsLookupFailed; + response.Summary = Constants.DnsLookupFailed; + response.Exception = e; + } + else if (e.Message.Contains("No such host is known")) + { + // Thrown when the endpoint specified (e.g. Service Bus namespace) is not found (DNS resolution fails) + // Can happen due to misconfiguration or when the resource cannot be discovered as it is is behind a private + // endpoint not accessible from this network + response.Status = ConnectionStringValidationResult.ResultStatus.DnsLookupFailed; + response.Summary = Constants.DnsLookupFailed; + response.Exception = e; + } + else if (e is ArgumentNullException || + e.Message.Contains("could not be found") || + e.Message.Contains("was not found")) + { + response.Status = ConnectionStringValidationResult.ResultStatus.MalformedConnectionString; + response.Summary = String.Format(Constants.MalformedConnectionStringDetails, appSettingName); + response.Details = Constants.GenericDetailsMessage; + response.Exception = e; + } + else if (e is ArgumentException && e.Message.Contains("entityPath is null") || + e.Message.Contains("HostNotFound") || + e.Message.Contains("could not be found") || + e.Message.Contains("The argument is null or white space")) + { + response.Status = ConnectionStringValidationResult.ResultStatus.MalformedConnectionString; + response.Summary = String.Format(Constants.MalformedConnectionStringDetails, appSettingName); + response.Details = Constants.GenericDetailsMessage; + response.Exception = e; + } + else if (e.Message.Contains("InvalidSignature")) + { + response.Status = ConnectionStringValidationResult.ResultStatus.AuthFailure; + response.Summary = String.Format(Constants.AuthFailureSummary, appSettingName) + " " + GetRelevantAuthFailureDocs(type); + response.Details = Constants.GenericDetailsMessage; + response.Exception = e; + } + else if ((e is ArgumentException && e.Message.Contains("Authentication")) || + e.Message.Contains("claim is empty or token is invalid") || + e.Message.Contains("InvalidSignature")) + { + response.Status = ConnectionStringValidationResult.ResultStatus.AuthFailure; + response.Summary = String.Format(Constants.AuthFailureSummary, appSettingName) + " " + GetRelevantAuthFailureDocs(type); + response.Details = Constants.GenericDetailsMessage; + response.Exception = e; + } + else if ((e is Azure.RequestFailedException && e.Message.Contains("failed to authenticate")) || + e.Message.Contains("claim is empty or token is invalid") || + e.Message.Contains("InvalidSignature")) + { + response.Status = ConnectionStringValidationResult.ResultStatus.AuthFailure; + response.Summary = String.Format(Constants.AuthFailureSummary, appSettingName) + " " + GetRelevantAuthFailureDocs(type); + response.Details = Constants.GenericDetailsMessage; + response.Exception = e; + } + else if (e.Message.Contains("Ip has been prevented to connect to the endpoint")) + { + response.Status = ConnectionStringValidationResult.ResultStatus.Forbidden; + if (e.Message.Contains("AuthenticationFailed")) + { + response.Summary = String.Format(Constants.AuthFailureSummary, appSettingName) + " " + GetRelevantAuthFailureDocs(type); + response.Details = Constants.GenericDetailsMessage; + } + else + { + response.Summary = "Access to the "+ type +" resource is restricted."; + switch (type) + { + case ConnectionStringType.ServiceBus: + response.Details = Constants.ServiceBusAccessRestrictedDetails; + break; + case ConnectionStringType.EventHubs: + response.Details = Constants.EventHubAccessRestrictedDetails; + break; + case ConnectionStringType.StorageAccount: + case ConnectionStringType.BlobStorageAccount: + case ConnectionStringType.QueueStorageAccount: + case ConnectionStringType.FileShareStorageAccount: + response.Details = Constants.StorageAccessRestrictedDetails; + break; + } + } + response.Exception = e; + } + else if (e is StorageException) + { + if (((StorageException)e).RequestInformation.HttpStatusCode == 401) + { + response.Status = ConnectionStringValidationResult.ResultStatus.AuthFailure; + response.Summary = String.Format(Constants.AuthFailureSummary, appSettingName) + " " + GetRelevantAuthFailureDocs(type); + response.Details = Constants.GenericDetailsMessage; + } + else if (((StorageException)e).RequestInformation.HttpStatusCode == 403) + { + response.Status = ConnectionStringValidationResult.ResultStatus.Forbidden; + if (e.Message.Contains("AuthenticationFailed")) + { + response.Summary = String.Format(Constants.AuthFailureSummary, appSettingName) + " " + GetRelevantAuthFailureDocs(type); + response.Details = Constants.GenericDetailsMessage; + } + else + { + response.Summary = "Access to the " + type + " resource is restricted."; + switch (type) + { + case ConnectionStringType.ServiceBus: + response.Details = Constants.ServiceBusAccessRestrictedDetails; + break; + case ConnectionStringType.EventHubs: + response.Details = Constants.EventHubAccessRestrictedDetails; + break; + case ConnectionStringType.StorageAccount: + case ConnectionStringType.BlobStorageAccount: + case ConnectionStringType.QueueStorageAccount: + case ConnectionStringType.FileShareStorageAccount: + response.Details = Constants.StorageAccessRestrictedDetails; + break; + } + } + } + response.Exception = e; + } + else + { + response.Status = ConnectionStringValidationResult.ResultStatus.UnknownError; + response.Summary = Constants.UnknownErrorSummary; + response.Exception = e; + } + } + public static bool IsKeyVaultReference(string value) + { + return value.Contains("@Microsoft.KeyVault"); + } + + private static string GetRelevantAuthFailureDocs(ConnectionStringType type) + { + switch (type) + { + case ConnectionStringType.BlobStorageAccount: + return Constants.AuthFailureBlobStorageDocs; + case ConnectionStringType.QueueStorageAccount: + return Constants.AuthFailureQueueStorageDocs; + case ConnectionStringType.FileShareStorageAccount: + return Constants.AuthFailureFileShareStorageDocs; + case ConnectionStringType.ServiceBus: + return Constants.AuthFailureServiceBusDocs; + case ConnectionStringType.EventHubs: + return Constants.AuthFailureEventHubsDocs; + default: + return ""; + } + } + } +} diff --git a/DiagnosticsExtension/Models/ConnectionStringValidator/ConnectionStringType.cs b/DiagnosticsExtension/Models/ConnectionStringValidator/ConnectionStringType.cs index db27c2f4..ea1be897 100644 --- a/DiagnosticsExtension/Models/ConnectionStringValidator/ConnectionStringType.cs +++ b/DiagnosticsExtension/Models/ConnectionStringValidator/ConnectionStringType.cs @@ -19,8 +19,11 @@ public enum ConnectionStringType KeyVault, Http, RedisCache, - StorageAccount, ServiceBus, - EventHubs + EventHubs, + StorageAccount, + BlobStorageAccount, + QueueStorageAccount, + FileShareStorageAccount, } -} \ No newline at end of file +} diff --git a/DiagnosticsExtension/Models/ConnectionStringValidator/ConnectionStringValidationResult.cs b/DiagnosticsExtension/Models/ConnectionStringValidator/ConnectionStringValidationResult.cs index f061e58b..7d6e66a6 100644 --- a/DiagnosticsExtension/Models/ConnectionStringValidator/ConnectionStringValidationResult.cs +++ b/DiagnosticsExtension/Models/ConnectionStringValidator/ConnectionStringValidationResult.cs @@ -8,17 +8,28 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Web; +using System.Web.UI.WebControls; +using Newtonsoft.Json; namespace DiagnosticsExtension.Models.ConnectionStringValidator { public class ConnectionStringValidationResult { + [Newtonsoft.Json.JsonIgnore] public ResultStatus? Status; + [Newtonsoft.Json.JsonIgnore] + public string IdentityType; + public string Summary; + public string Details; public string StatusText => Status?.ToString(); public Exception Exception; + public string ExceptionMessage => Exception?.Message; + [Newtonsoft.Json.JsonIgnore] public object Payload; + [Newtonsoft.Json.JsonIgnore] public string Type => type.ToString(); private ConnectionStringType type; @@ -40,9 +51,24 @@ public enum ResultStatus MsiFailure, EmptyConnectionString, MalformedConnectionString, - UnknownError + EntityNotFound, + FullyQualifiedNamespaceMissing, + ManagedIdentityNotConfigured, + ManagedIdentityAuthFailure, + ManagedIdentityConnectionFailed, + KeyVaultReferenceResolutionFailed, + UnknownError, } + public enum ManagedIdentityCommonProperty + { + fullyQualifiedNamespace, + credential, + clientId, + serviceUri, + blobServiceUri, + queueServiceUri + } + - } -} \ No newline at end of file +} diff --git a/DiagnosticsExtension/Models/ConnectionStringValidator/Constants.cs b/DiagnosticsExtension/Models/ConnectionStringValidator/Constants.cs new file mode 100644 index 00000000..37b3c701 --- /dev/null +++ b/DiagnosticsExtension/Models/ConnectionStringValidator/Constants.cs @@ -0,0 +1,64 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. +// +// ----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace DiagnosticsExtension.Models.ConnectionStringValidator +{ + public static class Constants + { + public const string User = "User"; + public const string System = "System"; + public const string UnderscoreSeperator = "__"; + public const string ColonSeparator = ":"; + public const string ClientId = "__clientId"; + public const string Credential = "__credential"; + public const string QueueServiceUri = "__queueServiceUri"; + public const string ValidCredentialValue = "managedidentity"; + public const string FullyQualifiedNamespace = "__fullyQualifiedNamespace"; + public const string UnknownErrorSummary = "Validation of connection string failed due to an unknown error."; + public const string GenericDetailsMessage = "Additional error details:"; + public const string ManagedIdentityTutorial = "Refer to this relevant tutorial."; + public const string BlobServiceUriMissingSummary = "Necessary connection settings not found. A connection string or identity-based connection settings are required. See relevant docs. "; + public const string QueueServiceUriMissingSummary = "Necessary connection settings not found. A connection string or identity-based connection settings are required. See relevant docs. "; + public const string ServiceBusFQMissingSummary = "Necessary connection settings not found. A connection string or identity-based connection settings are required. See relevant docs. "; + public const string EventHubFQMissingSummary = "Necessary connection settings not found. A connection string or identity-based connection settings are required. See relevant docs. "; + public const string BlobServiceUriEmptySummary = "The app setting '{0}' has no value. See relevant docs. " + ManagedIdentityTutorial; + public const string QueueServiceUriEmptySummary = "The app setting '{0}' has no value. See relevant docs. " + ManagedIdentityTutorial; + public const string ServiceBusFQNSEmptySummary = "The app setting '{0}' has no value. See relevant docs. " + ManagedIdentityTutorial; + public const string EventHubFQNSEmptySummary = "The app setting '{0}' has no value. See relevant docs. " + ManagedIdentityTutorial; + public const string ManagedIdentityClientIdEmptySummary = "The app setting '{0}' has no value."; + public const string ManagedIdentityClientIdEmptyDetails = "When the app setting '{0}'" + Credential + " is configured to \"managedidentity\", the clientId of a user assigned managed identity assigned to the Function App is expected in the app setting {0}" + ClientId + ". See relevant docs. " + ManagedIdentityTutorial; + public const string AuthorizationFailure = "Authorization failure"; + public const string AuthenticationFailure = "Authentication failure"; + public const string ClientIdInvalidTokenGeneratedSummary = "The value of the app setting '{0}'" + ClientId + " does not match any user assigned managed identity assigned to this app. See relevant docs. " + ManagedIdentityTutorial; + public const string SystemAssignedAuthFailure = "The system assigned managed identity for this Function App does not have access to the resource configured in '{0}'. See relevant docs. " + ManagedIdentityTutorial; + public const string UserAssignedAuthFailure = "The configured user assigned managed identity does not have access to the resource configured in '{0}'. See relevant docs. " + ManagedIdentityTutorial; + public const string AuthFailureSummary = "Authentication failure. Credentials in connection string configured in app setting '{0}' are invalid or expired."; + public const string AuthFailureBlobStorageDocs = "See relevant docs."; + public const string AuthFailureQueueStorageDocs = "See relevant docs."; + public const string AuthFailureFileShareStorageDocs = "See relevant docs."; + public const string AuthFailureServiceBusDocs = "See relevant docs."; + public const string AuthFailureEventHubsDocs = "See relevant docs."; + public const string StorageAccessRestrictedDetails = "This may be due to firewall rules on the resource. Please check if you have configured firewall rules or a private endpoint and that they correctly allow access from the Function App. See Storage account network security for additional details."; + public const string ServiceBusAccessRestrictedDetails = "This may be due to firewall rules on the resource. Please check if you have configured firewall rules or a private endpoint and that they correctly allow access from the Function App. See Service Bus network security for additional details."; + public const string EventHubAccessRestrictedDetails = "This may be due to firewall rules on the resource. Please check if you have configured firewall rules or a private endpoint and that they correctly allow access from the Function App. See Event Hubs network security for additional details."; + public const string DnsLookupFailed = "The service resource specified in the connection string was not found. Please check the value of the setting."; + public const string FQNamespaceResourceNotFound = "The resource specified in the app setting '{0}" + FullyQualifiedNamespace + "' was not found. Please check the value of the setting."; + public const string StorageAccountResourceNotFound = "The resource specified in the app setting '{0}' was not found. Please check the value of the setting."; + public const string MalformedConnectionStringDetails = "The connection string configured in app setting '{0}' is invalid. Please check the value of the setting."; + public const string EventHubEntityNotFoundSummary = "The configured Event Hub '{0}' was not found."; + public const string EventHubEntityNotFoundDetails = "Refer to relevant docs and check the value of the attribute 'EventHubName' in your code."; + public const string ServiceBusEntityNotFoundSummary = "The configured Queue or Topic '{0}' was not found."; + public const string ServiceBusEntityNotFoundDetails = "Refer to relevant docs and check the value of the attribute 'QueueName' or 'TopicName' in your code."; + public const string ManagedIdentityCredentialInvalidSummary = "The app setting '{0}" + Credential + "' is not valid. To use identity-based connections, set its value to \"managedidentity\". See relevant docs. " + ManagedIdentityTutorial; + public const string KeyVaultReferenceResolutionFailedSummary = "The Azure Key Vault reference configured in app setting '{0}' could not be resolved. See relevant docs."; + } +} diff --git a/DiagnosticsExtension/Models/ConnectionStringValidator/EventHubsValidator.cs b/DiagnosticsExtension/Models/ConnectionStringValidator/EventHubsValidator.cs index 88048727..4a26918a 100644 --- a/DiagnosticsExtension/Models/ConnectionStringValidator/EventHubsValidator.cs +++ b/DiagnosticsExtension/Models/ConnectionStringValidator/EventHubsValidator.cs @@ -10,15 +10,16 @@ using Microsoft.Azure.EventHubs; using System; using System.Threading.Tasks; +using Azure.Identity; +using Azure.Messaging.EventHubs.Producer; +using System.Linq; namespace DiagnosticsExtension.Models.ConnectionStringValidator { public class EventHubsValidator : IConnectionStringValidator { public string ProviderName => "Microsoft.Azure.EventHubs"; - public ConnectionStringType Type => ConnectionStringType.EventHubs; - public Task IsValidAsync(string connectionString) { try @@ -29,13 +30,12 @@ public Task IsValidAsync(string connectionString) { return Task.FromResult(false); } - return Task.FromResult(true); } async public Task ValidateAsync(string connectionString, string clientId = null) { - var response = new ConnectionStringValidationResult(Type); + ConnectionStringValidationResult response = new ConnectionStringValidationResult(Type); try { @@ -51,37 +51,7 @@ async public Task ValidateAsync(string connect } catch (Exception e) { - if (e is MalformedConnectionStringException) - { - response.Status = ConnectionStringValidationResult.ResultStatus.MalformedConnectionString; - } - else if (e is EmptyConnectionStringException) - { - response.Status = ConnectionStringValidationResult.ResultStatus.EmptyConnectionString; - } - else if (e is ArgumentNullException || - e.Message.Contains("could not be found") || - e.Message.Contains("was not found")) - { - response.Status = ConnectionStringValidationResult.ResultStatus.MalformedConnectionString; - } - else if (e.Message.Contains("No such host is known")) - { - response.Status = ConnectionStringValidationResult.ResultStatus.DnsLookupFailed; - } - else if (e.Message.Contains("InvalidSignature")) - { - response.Status = ConnectionStringValidationResult.ResultStatus.AuthFailure; - } - else if (e.Message.Contains("Ip has been prevented to connect to the endpoint")) - { - response.Status = ConnectionStringValidationResult.ResultStatus.Forbidden; - } - else - { - response.Status = ConnectionStringValidationResult.ResultStatus.UnknownError; - } - response.Exception = e; + ConnectionStringResponseUtility.EvaluateResponseStatus(e, Type, ref response); } return response; @@ -101,5 +71,107 @@ protected async Task TestConnectionStringAsync(string connec return data; } + + async public Task ValidateViaAppsettingAsync(string appSettingName, string entityName) + { + ConnectionStringValidationResult response = new ConnectionStringValidationResult(Type); + bool isManagedIdentityConnection = false; + try + { + string appSettingClientIdValue, appSettingClientCredValue = ""; + EventHubProducerClient client = null; + var envDict = Environment.GetEnvironmentVariables(); + string eventHubName = entityName; + + if (envDict.Contains(appSettingName)) + { + try + { + string connectionString = Environment.GetEnvironmentVariable(appSettingName); + if (string.IsNullOrEmpty(connectionString)) + { + throw new EmptyConnectionStringException(); + } + connectionString += ";EntityPath=" + eventHubName; + client = new EventHubProducerClient(connectionString); + } + catch (EmptyConnectionStringException e) + { + throw new EmptyConnectionStringException(e.Message, e); + } + catch (Exception e) + { + throw new MalformedConnectionStringException(e.Message, e); + } + } + else + { + isManagedIdentityConnection = true; + string serviceUriString = ManagedIdentityConnectionResponseUtility.ResolveManagedIdentityCommonProperty(appSettingName, ConnectionStringValidationResult.ManagedIdentityCommonProperty.fullyQualifiedNamespace); + if (!string.IsNullOrEmpty(serviceUriString)) + { + string clientIdAppSettingKey = Environment.GetEnvironmentVariables().Keys.Cast().Where(k => k.StartsWith(appSettingName) && k.ToLower().EndsWith("clientid")).FirstOrDefault(); + appSettingClientIdValue = ManagedIdentityConnectionResponseUtility.ResolveManagedIdentityCommonProperty(appSettingName, ConnectionStringValidationResult.ManagedIdentityCommonProperty.clientId); + appSettingClientCredValue = ManagedIdentityConnectionResponseUtility.ResolveManagedIdentityCommonProperty(appSettingName, ConnectionStringValidationResult.ManagedIdentityCommonProperty.credential); + if (appSettingClientCredValue != null && appSettingClientCredValue != Constants.ValidCredentialValue) + { + throw new ManagedIdentityException(String.Format(Constants.ManagedIdentityCredentialInvalidSummary, appSettingName)); + } + // If the user has configured __credential with "managedidentity" and set an app setting for __clientId (even if its empty) we assume their intent is to use a user assigned managed identity + if (appSettingClientCredValue != null && clientIdAppSettingKey != null) + { + if (string.IsNullOrEmpty(appSettingClientIdValue)) + { + throw new ManagedIdentityException(String.Format(Constants.ManagedIdentityClientIdEmptySummary, clientIdAppSettingKey), + String.Format(Constants.ManagedIdentityClientIdEmptyDetails, appSettingName)); + } + response.IdentityType = Constants.User; + client = new EventHubProducerClient(serviceUriString, eventHubName, ManagedIdentityCredentialTokenValidator.GetValidatedCredential(appSettingClientIdValue, appSettingName)); + } + // Creating client using System assigned managed identity + else + { + response.IdentityType = Constants.System; + client = new EventHubProducerClient(serviceUriString, eventHubName, new ManagedIdentityCredential()); + } + } + else + { + string fullyQualifiedNamespaceAppSettingName = Environment.GetEnvironmentVariables().Keys.Cast().Where(k => k.StartsWith(appSettingName) && k.ToLower().EndsWith("fullyqualifiednamespace")).FirstOrDefault(); + if (fullyQualifiedNamespaceAppSettingName == null) + { + throw new ManagedIdentityException(Constants.EventHubFQMissingSummary); + } + throw new ManagedIdentityException(String.Format(Constants.EventHubFQNSEmptySummary, fullyQualifiedNamespaceAppSettingName)); + + } + } + await client.GetPartitionIdsAsync(); + await client.CloseAsync(); + + response.Status = ConnectionStringValidationResult.ResultStatus.Success; + } + catch (Exception e) + { + // TODO: Find out what exception class is thrown for the message below and add that to the set of conditions + if (e.Message.Contains("The messaging entity") && e.Message.Contains("could not be found")) + { + response.Status = ConnectionStringValidationResult.ResultStatus.EntityNotFound; + response.Summary = String.Format(Constants.EventHubEntityNotFoundSummary, entityName); + response.Details = Constants.EventHubEntityNotFoundDetails; + response.Exception = e; + } + else if (isManagedIdentityConnection) + { + ManagedIdentityConnectionResponseUtility.EvaluateResponseStatus(e, Type, ref response, appSettingName); + } + else + { + ConnectionStringResponseUtility.EvaluateResponseStatus(e, Type, ref response, appSettingName); + } + } + + return response; + } } } diff --git a/DiagnosticsExtension/Models/ConnectionStringValidator/Exceptions/ManagedIdentityException.cs b/DiagnosticsExtension/Models/ConnectionStringValidator/Exceptions/ManagedIdentityException.cs new file mode 100644 index 00000000..ef4fdd7a --- /dev/null +++ b/DiagnosticsExtension/Models/ConnectionStringValidator/Exceptions/ManagedIdentityException.cs @@ -0,0 +1,30 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. +// +// ----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace DiagnosticsExtension.Models.ConnectionStringValidator.Exceptions +{ + public class ManagedIdentityException : Exception + { + public string MessageSummary { get; } + public string MessageDetails { get; } + + public ManagedIdentityException() : base() + { + } + + public ManagedIdentityException(string summary, string details = null) : base() + { + this.MessageSummary = summary; + this.MessageDetails = details; + } + } +} diff --git a/DiagnosticsExtension/Models/ConnectionStringValidator/FileShareStorageValidator.cs b/DiagnosticsExtension/Models/ConnectionStringValidator/FileShareStorageValidator.cs new file mode 100644 index 00000000..0c4780f8 --- /dev/null +++ b/DiagnosticsExtension/Models/ConnectionStringValidator/FileShareStorageValidator.cs @@ -0,0 +1,68 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. +// +// ----------------------------------------------------------------------- + +using DiagnosticsExtension.Controllers; +using DiagnosticsExtension.Models.ConnectionStringValidator.Exceptions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Azure.Storage.Blobs; +using Azure.Storage.Queues; +using Azure.Storage.Files; +using Azure.Storage.Files.Shares; +using Microsoft.WindowsAzure.Storage; + +namespace DiagnosticsExtension.Models.ConnectionStringValidator +{ + public class FileShareStorageValidator : IConnectionStringValidator + { + public string ProviderName => "Microsoft.WindowsAzure.Storage"; + public ConnectionStringType Type => ConnectionStringType.FileShareStorageAccount; + public async Task ValidateViaAppsettingAsync(string appSettingName, string entityName) + { + ConnectionStringValidationResult response = new ConnectionStringValidationResult(Type); + try + { + var envDict = Environment.GetEnvironmentVariables(); + ShareServiceClient client = null; + try + { + if (envDict.Contains(appSettingName)) + { + string connectionString = Environment.GetEnvironmentVariable(appSettingName); + client = new ShareServiceClient(connectionString); + } + } + catch (ArgumentNullException e) + { + throw new EmptyConnectionStringException(e.Message, e); + } + catch (Exception e) + { + throw new MalformedConnectionStringException(e.Message, e); + } + client.GetSharesAsync(); + response.Status = ConnectionStringValidationResult.ResultStatus.Success; + } + catch (Exception e) + { + ConnectionStringResponseUtility.EvaluateResponseStatus(e, Type, ref response, appSettingName); + } + + return response; + } + public async Task ValidateAsync(string connStr, string clientId = null) + { + throw new NotImplementedException(); + } + public async Task IsValidAsync(string connStr) + { + throw new NotImplementedException(); + } + } +} diff --git a/DiagnosticsExtension/Models/ConnectionStringValidator/HttpValidator.cs b/DiagnosticsExtension/Models/ConnectionStringValidator/HttpValidator.cs index 79e6f86e..66e4a456 100644 --- a/DiagnosticsExtension/Models/ConnectionStringValidator/HttpValidator.cs +++ b/DiagnosticsExtension/Models/ConnectionStringValidator/HttpValidator.cs @@ -103,5 +103,9 @@ async public Task ValidateAsync(string connStr return response; } + public async Task ValidateViaAppsettingAsync(string appsettingName, string entityName) + { + throw new NotImplementedException(); + } } } diff --git a/DiagnosticsExtension/Models/ConnectionStringValidator/IConnectionStringValidator.cs b/DiagnosticsExtension/Models/ConnectionStringValidator/IConnectionStringValidator.cs index 106ad24d..7bf1d9a1 100644 --- a/DiagnosticsExtension/Models/ConnectionStringValidator/IConnectionStringValidator.cs +++ b/DiagnosticsExtension/Models/ConnectionStringValidator/IConnectionStringValidator.cs @@ -19,7 +19,7 @@ interface IConnectionStringValidator Task IsValidAsync(string connStr); Task ValidateAsync(string connStr, string clientId = null); // clientId used for Used Assigned Managed Identity - + Task ValidateViaAppsettingAsync(string appSettingName, string entityName = null); string ProviderName { get; } ConnectionStringType Type { get; } diff --git a/DiagnosticsExtension/Models/ConnectionStringValidator/KeyVaultValidator.cs b/DiagnosticsExtension/Models/ConnectionStringValidator/KeyVaultValidator.cs index 54d8b178..38d33b9f 100644 --- a/DiagnosticsExtension/Models/ConnectionStringValidator/KeyVaultValidator.cs +++ b/DiagnosticsExtension/Models/ConnectionStringValidator/KeyVaultValidator.cs @@ -155,5 +155,9 @@ async public Task ValidateAsync(string connStr return response; } + public async Task ValidateViaAppsettingAsync(string appsettingName, string entityName) + { + throw new NotImplementedException(); + } } } diff --git a/DiagnosticsExtension/Models/ConnectionStringValidator/ManagedIdentityConnectionResponseUtility.cs b/DiagnosticsExtension/Models/ConnectionStringValidator/ManagedIdentityConnectionResponseUtility.cs new file mode 100644 index 00000000..5bcc599d --- /dev/null +++ b/DiagnosticsExtension/Models/ConnectionStringValidator/ManagedIdentityConnectionResponseUtility.cs @@ -0,0 +1,105 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. +// +// ----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using Azure; +using Azure.Identity; +using Microsoft.WindowsAzure.Storage; +using DiagnosticsExtension.Models.ConnectionStringValidator.Exceptions; + +namespace DiagnosticsExtension.Models.ConnectionStringValidator +{ + public static class ManagedIdentityConnectionResponseUtility + { + public static void EvaluateResponseStatus(Exception e, ConnectionStringType type, ref ConnectionStringValidationResult response, string appSettingName = "") + { + if (e is ManagedIdentityException) + { + response.Status = ConnectionStringValidationResult.ResultStatus.ManagedIdentityConnectionFailed; + response.Summary = ((ManagedIdentityException)e).MessageSummary; + response.Details = ((ManagedIdentityException)e).MessageDetails; + } + else if (e is UnauthorizedAccessException && e.Message.Contains("unauthorized") || e.Message.Contains("Unauthorized") || e.Message.Contains("request is not authorized")) + { + response.Status = ConnectionStringValidationResult.ResultStatus.ManagedIdentityAuthFailure; + response.Summary = Constants.AuthorizationFailure; + if (response.IdentityType == "System") + { + response.Details = String.Format(Constants.SystemAssignedAuthFailure, GetTargetConnectionAppSettingName(type, appSettingName)); + } + else + { + response.Details = String.Format(Constants.UserAssignedAuthFailure, GetTargetConnectionAppSettingName(type, appSettingName)); + } + + response.Exception = e; + } + else if (e is AuthenticationFailedException && e.Message.Contains("ManagedIdentityCredential")) + { + response.Status = ConnectionStringValidationResult.ResultStatus.ManagedIdentityNotConfigured; + response.Summary = "Your app is configured to use identity based connection but does not have a system assigned managed identity assigned. Refer here for details."; + } + else if (e.Message.Contains("fullyQualifiedNamespace")) + { + response.Status = ConnectionStringValidationResult.ResultStatus.FullyQualifiedNamespaceMissing; + response.Summary = "The app setting " + appSettingName + "__fullyQualifiedNamespace was not found or is set to a blank value. Refer here for details."; + } + else if (e.InnerException != null && + e.InnerException.Message.Contains("The remote name could not be resolved")) // queue and blob + { + + response.Status = ConnectionStringValidationResult.ResultStatus.DnsLookupFailed; + response.Summary = String.Format(Constants.StorageAccountResourceNotFound, GetTargetConnectionAppSettingName(type, appSettingName)); + } + else if (e.Message.Contains("No such host is known")) // event hub and service bus + { + response.Status = ConnectionStringValidationResult.ResultStatus.DnsLookupFailed; + response.Summary = String.Format(Constants.FQNamespaceResourceNotFound, appSettingName); + response.Details = Constants.GenericDetailsMessage; + response.Exception = e; + } + else + { + response.Status = ConnectionStringValidationResult.ResultStatus.UnknownError; + response.Summary = Constants.UnknownErrorSummary; + response.Exception = e; + } + } + public static string ResolveManagedIdentityCommonProperty(string appSettingName, ConnectionStringValidationResult.ManagedIdentityCommonProperty prop) + { + string commonPropertyValue = Environment.GetEnvironmentVariable(appSettingName + Constants.UnderscoreSeperator + prop.ToString()); + + if (commonPropertyValue == null) + { + commonPropertyValue = Environment.GetEnvironmentVariable(appSettingName + Constants.ColonSeparator + prop.ToString()); + } + return commonPropertyValue; + } + public static string GetTargetConnectionAppSettingName(ConnectionStringType type, string appSettingName) + { + string serviceuriAppSettingName = ""; + switch (type) + { + + case ConnectionStringType.ServiceBus: + case ConnectionStringType.EventHubs: + serviceuriAppSettingName = Environment.GetEnvironmentVariables().Keys.Cast().Where(k => k.StartsWith(appSettingName) && k.ToLower().EndsWith("fullyqualifiednamespace")).FirstOrDefault(); + + break; + case ConnectionStringType.BlobStorageAccount: + case ConnectionStringType.QueueStorageAccount: + serviceuriAppSettingName = Environment.GetEnvironmentVariables().Keys.Cast().Where(k => k.StartsWith(appSettingName) && k.ToLower().EndsWith("serviceuri")).FirstOrDefault(); + + break; + } + return serviceuriAppSettingName; + } + } +} diff --git a/DiagnosticsExtension/Models/ConnectionStringValidator/ManagedIdentityCredentialTokenValidator.cs b/DiagnosticsExtension/Models/ConnectionStringValidator/ManagedIdentityCredentialTokenValidator.cs new file mode 100644 index 00000000..bef8db68 --- /dev/null +++ b/DiagnosticsExtension/Models/ConnectionStringValidator/ManagedIdentityCredentialTokenValidator.cs @@ -0,0 +1,41 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. +// +// ----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Web; +using Azure.Core; +using Azure.Identity; +using DiagnosticsExtension.Models.ConnectionStringValidator.Exceptions; + +namespace DiagnosticsExtension.Models.ConnectionStringValidator +{ + public static class ManagedIdentityCredentialTokenValidator + { + public static ManagedIdentityCredential GetValidatedCredential(string clientId, string appSettingName) + { + var tokenCredential = new ManagedIdentityCredential(clientId); + + var accessToken = tokenCredential.GetTokenAsync(new TokenRequestContext(scopes: new string[] { "https://storage.azure.com/.default" }) { }); + + JwtSecurityTokenHandler jwtHandler = new JwtSecurityTokenHandler(); + + string appId = jwtHandler.ReadJwtToken(accessToken.Result.Token).Claims.First(x => x.Type == "appid").Value; + + if (appId != clientId) + { + throw new ManagedIdentityException(String.Format(Constants.ClientIdInvalidTokenGeneratedSummary, appSettingName)); + } + + return tokenCredential; + } + + } + +} diff --git a/DiagnosticsExtension/Models/ConnectionStringValidator/MySqlValidator.cs b/DiagnosticsExtension/Models/ConnectionStringValidator/MySqlValidator.cs index d928eb37..05c2e007 100644 --- a/DiagnosticsExtension/Models/ConnectionStringValidator/MySqlValidator.cs +++ b/DiagnosticsExtension/Models/ConnectionStringValidator/MySqlValidator.cs @@ -111,5 +111,9 @@ public async Task TestMySqlConnectionString(string connectio return data; } + public async Task ValidateViaAppsettingAsync(string appsettingName, string entityName) + { + throw new NotImplementedException(); + } } } diff --git a/DiagnosticsExtension/Models/ConnectionStringValidator/QueueStorageValidator.cs b/DiagnosticsExtension/Models/ConnectionStringValidator/QueueStorageValidator.cs new file mode 100644 index 00000000..4a24095f --- /dev/null +++ b/DiagnosticsExtension/Models/ConnectionStringValidator/QueueStorageValidator.cs @@ -0,0 +1,129 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. +// +// ----------------------------------------------------------------------- + +using DiagnosticsExtension.Controllers; +using DiagnosticsExtension.Models.ConnectionStringValidator.Exceptions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Azure.Storage.Blobs; +using Azure.Storage.Queues; +using Azure.Storage.Files; +using Azure.Storage.Queues.Models; +using Microsoft.WindowsAzure.Storage; + +namespace DiagnosticsExtension.Models.ConnectionStringValidator +{ + public class QueueStorageValidator : IConnectionStringValidator + { + public string ProviderName => "Microsoft.WindowsAzure.Storage"; + public ConnectionStringType Type => ConnectionStringType.QueueStorageAccount; + public async Task ValidateViaAppsettingAsync(string appSettingName, string entityName) + { + ConnectionStringValidationResult response = new ConnectionStringValidationResult(Type); + bool isManagedIdentityConnection = false; + try + { + var envDict = Environment.GetEnvironmentVariables(); + string appSettingClientIdValue, appSettingClientCredValue = null; + QueueServiceClient client = null; + + if (envDict.Contains(appSettingName)) + { + try + { + string connectionString = Environment.GetEnvironmentVariable(appSettingName); + client = new QueueServiceClient(connectionString); + } + catch (ArgumentNullException e) + { + throw new EmptyConnectionStringException(e.Message, e); + } + catch (Exception e) + { + throw new MalformedConnectionStringException(e.Message, e); + } + } + else + { + isManagedIdentityConnection = true; + string serviceUriString = ManagedIdentityConnectionResponseUtility.ResolveManagedIdentityCommonProperty(appSettingName, ConnectionStringValidationResult.ManagedIdentityCommonProperty.queueServiceUri); + if (!string.IsNullOrEmpty(serviceUriString)) + { + string clientIdAppSettingKey = Environment.GetEnvironmentVariables().Keys.Cast().Where(k => k.StartsWith(appSettingName) && k.ToLower().EndsWith("clientid")).FirstOrDefault(); + appSettingClientIdValue = ManagedIdentityConnectionResponseUtility.ResolveManagedIdentityCommonProperty(appSettingName, ConnectionStringValidationResult.ManagedIdentityCommonProperty.clientId); + appSettingClientCredValue = ManagedIdentityConnectionResponseUtility.ResolveManagedIdentityCommonProperty(appSettingName, ConnectionStringValidationResult.ManagedIdentityCommonProperty.credential); + if (appSettingClientCredValue != null && appSettingClientCredValue != Constants.ValidCredentialValue) + { + throw new ManagedIdentityException(String.Format(Constants.ManagedIdentityCredentialInvalidSummary, appSettingName)); + } + Uri serviceUri = new Uri(serviceUriString); + // If the user has configured __credential with "managedidentity" and set an app setting for __clientId (even if its empty) we assume their intent is to use a user assigned managed identity + if (appSettingClientCredValue != null && clientIdAppSettingKey != null) + { + if (string.IsNullOrEmpty(appSettingClientIdValue)) + { + throw new ManagedIdentityException(String.Format(Constants.ManagedIdentityClientIdEmptySummary, clientIdAppSettingKey), + String.Format(Constants.ManagedIdentityClientIdEmptyDetails, appSettingName)); + } + response.IdentityType = Constants.User; + client = new QueueServiceClient(serviceUri, ManagedIdentityCredentialTokenValidator.GetValidatedCredential(appSettingClientIdValue, appSettingName)); + } + // Creating client using System assigned managed identity + else + { + response.IdentityType = Constants.System; + client = new QueueServiceClient(serviceUri, new Azure.Identity.ManagedIdentityCredential()); + } + } + else + { + string serviceuriAppSettingName = Environment.GetEnvironmentVariables().Keys.Cast().Where(k => k.StartsWith(appSettingName) && k.ToLower().EndsWith("queueserviceuri")).FirstOrDefault(); + if (serviceuriAppSettingName == null) + { + throw new ManagedIdentityException(Constants.QueueServiceUriMissingSummary); + } + throw new ManagedIdentityException(String.Format(Constants.QueueServiceUriEmptySummary, appSettingName)); + } + } + var resultSegment = + client.GetQueues(QueueTraits.Metadata, null, default) + .AsPages(default, 10); + foreach (Azure.Page containerPage in resultSegment) + { + foreach (QueueItem containerItem in containerPage.Values) + { + string containerName = containerItem.Name.ToString(); + } + } + response.Status = ConnectionStringValidationResult.ResultStatus.Success; + } + catch (Exception e) + { + if (isManagedIdentityConnection) + { + ManagedIdentityConnectionResponseUtility.EvaluateResponseStatus(e, Type, ref response, appSettingName); + } + else + { + ConnectionStringResponseUtility.EvaluateResponseStatus(e, Type, ref response, appSettingName); + } + } + + return response; + } + public async Task ValidateAsync(string connStr, string clientId = null) + { + throw new NotImplementedException(); + } + public async Task IsValidAsync(string connStr) + { + throw new NotImplementedException(); + } + } +} diff --git a/DiagnosticsExtension/Models/ConnectionStringValidator/ServiceBusValidator.cs b/DiagnosticsExtension/Models/ConnectionStringValidator/ServiceBusValidator.cs index 42b1986e..38cd588d 100644 --- a/DiagnosticsExtension/Models/ConnectionStringValidator/ServiceBusValidator.cs +++ b/DiagnosticsExtension/Models/ConnectionStringValidator/ServiceBusValidator.cs @@ -13,13 +13,13 @@ using System; using System.Linq; using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; namespace DiagnosticsExtension.Models.ConnectionStringValidator { public class ServiceBusValidator : IConnectionStringValidator { public string ProviderName => "Microsoft.Azure.ServiceBus"; - public ConnectionStringType Type => ConnectionStringType.ServiceBus; public Task IsValidAsync(string connectionString) @@ -37,7 +37,7 @@ public Task IsValidAsync(string connectionString) async public Task ValidateAsync(string connectionString, string clientId = null) { - var response = new ConnectionStringValidationResult(Type); + ConnectionStringValidationResult response = new ConnectionStringValidationResult(Type); try { @@ -53,41 +53,7 @@ async public Task ValidateAsync(string connect } catch (Exception e) { - if (e is MalformedConnectionStringException || e is ArgumentNullException) - { - response.Status = ConnectionStringValidationResult.ResultStatus.MalformedConnectionString; - } - else if (e is EmptyConnectionStringException) - { - response.Status = ConnectionStringValidationResult.ResultStatus.EmptyConnectionString; - } - else if ((e is ArgumentException && e.Message.Contains("Authentication ")) || - e.Message.Contains("claim is empty or token is invalid") || - e.Message.Contains("InvalidSignature")) - { - response.Status = ConnectionStringValidationResult.ResultStatus.AuthFailure; - } - else if (e is ArgumentException && e.Message.Contains("entityPath is null") || - e.Message.Contains("HostNotFound") || - e.Message.Contains("could not be found") || - e.Message.Contains("The argument is null or white space")) - { - response.Status = ConnectionStringValidationResult.ResultStatus.MalformedConnectionString; - } - else if (e.InnerException != null && e.InnerException.InnerException != null && - e.InnerException.InnerException.Message.Contains("The remote name could not be resolved")) - { - response.Status = ConnectionStringValidationResult.ResultStatus.DnsLookupFailed; - } - else if (e.Message.Contains("Ip has been prevented to connect to the endpoint")) - { - response.Status = ConnectionStringValidationResult.ResultStatus.Forbidden; - } - else - { - response.Status = ConnectionStringValidationResult.ResultStatus.UnknownError; - } - response.Exception = e; + ConnectionStringResponseUtility.EvaluateResponseStatus(e, Type, ref response); } return response; @@ -120,5 +86,108 @@ protected async Task TestConnectionStringAsync(string connec return data; } + async public Task ValidateViaAppsettingAsync(string appSettingName, string entityName) + { + ConnectionStringValidationResult response = new ConnectionStringValidationResult(Type); + bool isManagedIdentityConnection = false; + try + { + string appSettingClientIdValue, appSettingClientCredValue = ""; + ServiceBusClient client = null; + var envDict = Environment.GetEnvironmentVariables(); + + if (envDict.Contains(appSettingName)) + { + try + { + string connectionString = Environment.GetEnvironmentVariable(appSettingName); + if (string.IsNullOrEmpty(connectionString)) + { + throw new EmptyConnectionStringException(); + } + connectionString += ";EntityPath=" + entityName; + client = new ServiceBusClient(connectionString); + } + catch (EmptyConnectionStringException e) + { + throw new EmptyConnectionStringException(e.Message, e); + } + catch (Exception e) + { + throw new MalformedConnectionStringException(e.Message, e); + } + } + else + { + isManagedIdentityConnection = true; + string serviceUriString = ManagedIdentityConnectionResponseUtility.ResolveManagedIdentityCommonProperty(appSettingName, ConnectionStringValidationResult.ManagedIdentityCommonProperty.fullyQualifiedNamespace); + if (!string.IsNullOrEmpty(serviceUriString)) + { + string clientIdAppSettingKey = Environment.GetEnvironmentVariables().Keys.Cast().Where(k => k.StartsWith(appSettingName) && k.ToLower().EndsWith("clientid")).FirstOrDefault(); + appSettingClientIdValue = ManagedIdentityConnectionResponseUtility.ResolveManagedIdentityCommonProperty(appSettingName, ConnectionStringValidationResult.ManagedIdentityCommonProperty.clientId); + appSettingClientCredValue = ManagedIdentityConnectionResponseUtility.ResolveManagedIdentityCommonProperty(appSettingName, ConnectionStringValidationResult.ManagedIdentityCommonProperty.credential); + if (appSettingClientCredValue != null && appSettingClientCredValue != Constants.ValidCredentialValue) + { + throw new ManagedIdentityException(String.Format(Constants.ManagedIdentityCredentialInvalidSummary, appSettingName)); + } + // If the user has configured __credential with "managedidentity" and set an app setting for __clientId (even if its empty) we assume their intent is to use a user assigned managed identity + if (appSettingClientCredValue != null && clientIdAppSettingKey != null) + { + if (string.IsNullOrEmpty(appSettingClientIdValue)) + { + throw new ManagedIdentityException(String.Format(Constants.ManagedIdentityClientIdEmptySummary, clientIdAppSettingKey), + String.Format(Constants.ManagedIdentityClientIdEmptyDetails, appSettingName)); + } + response.IdentityType = Constants.User; + client = new ServiceBusClient(serviceUriString, ManagedIdentityCredentialTokenValidator.GetValidatedCredential(appSettingClientIdValue, appSettingName)); + } + // Creating client using System assigned managed identity + else + { + response.IdentityType = Constants.System; + client = new ServiceBusClient(serviceUriString, new Azure.Identity.ManagedIdentityCredential()); + } + } + else + { + string fullyQualifiedNamespaceAppSettingName = Environment.GetEnvironmentVariables().Keys.Cast().Where(k => k.StartsWith(appSettingName) && k.ToLower().EndsWith("fullyqualifiednamespace")).FirstOrDefault(); + if (fullyQualifiedNamespaceAppSettingName == null) + { + throw new ManagedIdentityException(Constants.ServiceBusFQMissingSummary); + } + throw new ManagedIdentityException(String.Format(Constants.ServiceBusFQNSEmptySummary, fullyQualifiedNamespaceAppSettingName)); + + } + } + ServiceBusReceiverOptions opt = new ServiceBusReceiverOptions(); + opt.ReceiveMode = ServiceBusReceiveMode.PeekLock; + opt.PrefetchCount = 1; + ServiceBusReceiver receiver = client.CreateReceiver(entityName, opt); + ServiceBusReceivedMessage receivedMessage = await receiver.PeekMessageAsync(); + + response.Status = ConnectionStringValidationResult.ResultStatus.Success; + } + catch (Exception e) + { + // TODO: Find out what exception class is thrown for the message below and add that to the set of conditions + if (e.Message.Contains("Put token failed") && e.Message.Contains("could not be found")) + { + response.Status = ConnectionStringValidationResult.ResultStatus.EntityNotFound; + response.Summary = String.Format(Constants.ServiceBusEntityNotFoundSummary, entityName); + response.Details = Constants.ServiceBusEntityNotFoundDetails; + response.Exception = e; + } + else if (isManagedIdentityConnection) + { + ManagedIdentityConnectionResponseUtility.EvaluateResponseStatus(e, Type, ref response, appSettingName); + } + else + { + ConnectionStringResponseUtility.EvaluateResponseStatus(e, Type, ref response, appSettingName); + } + } + + return response; + } } } diff --git a/DiagnosticsExtension/Models/ConnectionStringValidator/SqlServerValidator.cs b/DiagnosticsExtension/Models/ConnectionStringValidator/SqlServerValidator.cs index 99927e81..76a25ac4 100644 --- a/DiagnosticsExtension/Models/ConnectionStringValidator/SqlServerValidator.cs +++ b/DiagnosticsExtension/Models/ConnectionStringValidator/SqlServerValidator.cs @@ -165,5 +165,9 @@ public async Task TestSqlServerConnectionString(string conne return data; } + public async Task ValidateViaAppsettingAsync(string appsettingName, string entityName) + { + throw new NotImplementedException(); + } } } diff --git a/DiagnosticsExtension/Models/ConnectionStringValidator/StorageValidator.cs b/DiagnosticsExtension/Models/ConnectionStringValidator/StorageValidator.cs index 706fc322..cb98aeec 100644 --- a/DiagnosticsExtension/Models/ConnectionStringValidator/StorageValidator.cs +++ b/DiagnosticsExtension/Models/ConnectionStringValidator/StorageValidator.cs @@ -115,5 +115,9 @@ public Task TestConnectionString(string connectionString, st return Task.FromResult(data); } + public async Task ValidateViaAppsettingAsync(string appsettingName, string entityName) + { + throw new NotImplementedException(); + } } } diff --git a/DiagnosticsExtension/Web.config b/DiagnosticsExtension/Web.config index 7bdd8e0f..d494781f 100644 --- a/DiagnosticsExtension/Web.config +++ b/DiagnosticsExtension/Web.config @@ -102,11 +102,11 @@ - + - + @@ -122,7 +122,87 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DiagnosticsExtension/packages.config b/DiagnosticsExtension/packages.config index e262cc06..69154da5 100644 --- a/DiagnosticsExtension/packages.config +++ b/DiagnosticsExtension/packages.config @@ -1,6 +1,16 @@  + + + + + + + + + + @@ -18,21 +28,24 @@ - + + + + - - - + + + @@ -44,27 +57,36 @@ + - - + + - + + + - + + - + + + - + + + + - \ No newline at end of file +