From 9a13af426b2f6a9514c4ecfd814c4c1c187c7eaa Mon Sep 17 00:00:00 2001 From: zedy Date: Thu, 15 Dec 2022 15:41:24 +0800 Subject: [PATCH] Fix format and update the method of pulling connection string from kv with dac --- src/Infrastructure/Dependencies.cs | 1 - src/Web/AzureDeveloperCliCredential.cs | 148 +++++++++++++++++++++++++ src/Web/Program.cs | 16 ++- src/Web/Web.csproj | 2 + 4 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 src/Web/AzureDeveloperCliCredential.cs diff --git a/src/Infrastructure/Dependencies.cs b/src/Infrastructure/Dependencies.cs index 954da48..d049a96 100644 --- a/src/Infrastructure/Dependencies.cs +++ b/src/Infrastructure/Dependencies.cs @@ -11,7 +11,6 @@ public static class Dependencies public static void ConfigureServices(IConfiguration configuration, IServiceCollection services) { var useOnlyInMemoryDatabase = false; - if (configuration["UseOnlyInMemoryDatabase"] != null) { useOnlyInMemoryDatabase = bool.Parse(configuration["UseOnlyInMemoryDatabase"]); diff --git a/src/Web/AzureDeveloperCliCredential.cs b/src/Web/AzureDeveloperCliCredential.cs new file mode 100644 index 0000000..406c7bf --- /dev/null +++ b/src/Web/AzureDeveloperCliCredential.cs @@ -0,0 +1,148 @@ +using Azure.Core; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace Azure.Identity +{ + public class AzureDeveloperCliCredential : TokenCredential + { + internal const string AzdCliNotInstalled = $"Azure Developer CLI could not be found. {Troubleshoot}"; + internal const string AzdNotLogIn = "Please run 'azd login' from a command prompt to authenticate before using this credential."; + internal const string WinAzdCliError = "'azd is not recognized"; + internal const string AzdCliTimeoutError = "Azure Developer CLI authentication timed out."; + internal const string AzdCliFailedError = "Azure Developer CLI authentication failed due to an unknown error."; + internal const string Troubleshoot = "Please visit https://aka.ms/azure-dev for installation instructions and then, once installed, authenticate to your Azure account using 'azd login'."; + internal const string InteractiveLoginRequired = "Azure Developer CLI could not login. Interactive login is required."; + private const string RefreshTokeExpired = "The provided authorization code or refresh token has expired due to inactivity. Send a new interactive authorization request for this user and resource."; + + private static readonly string DefaultWorkingDirWindows = Environment.GetFolderPath(Environment.SpecialFolder.System); + private const string DefaultWorkingDirNonWindows = "/bin/"; + private static readonly string DefaultWorkingDir = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? DefaultWorkingDirWindows : DefaultWorkingDirNonWindows; + + private static readonly Regex AzdNotFoundPattern = new Regex("azd:(.*)not found"); + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken = default) + { + return RequestCliAccessTokenAsync(requestContext, cancellationToken) + .GetAwaiter() + .GetResult(); + } + + public override async ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken = default) + { + return await RequestCliAccessTokenAsync(requestContext, cancellationToken).ConfigureAwait(false); + } + + private async ValueTask RequestCliAccessTokenAsync(TokenRequestContext context, CancellationToken cancellationToken) + { + try + { + ProcessStartInfo processStartInfo = GetAzdCliProcessStartInfo(context.Scopes); + string output = await RunProcessAsync(processStartInfo); + + return DeserializeOutput(output); + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + throw new AuthenticationFailedException(AzdCliTimeoutError); + } + catch (InvalidOperationException exception) + { + bool isWinError = exception.Message.StartsWith(WinAzdCliError, StringComparison.CurrentCultureIgnoreCase); + bool isOtherOsError = AzdNotFoundPattern.IsMatch(exception.Message); + + if (isWinError || isOtherOsError) + { + throw new CredentialUnavailableException(AzdCliNotInstalled); + } + + bool isAADSTSError = exception.Message.Contains("AADSTS"); + bool isLoginError = exception.Message.IndexOf("azd login", StringComparison.OrdinalIgnoreCase) != -1; + + if (isLoginError && !isAADSTSError) + { + throw new CredentialUnavailableException(AzdNotLogIn); + } + + bool isRefreshTokenFailedError = exception.Message.IndexOf(AzdCliFailedError, StringComparison.OrdinalIgnoreCase) != -1 && + exception.Message.IndexOf(RefreshTokeExpired, StringComparison.OrdinalIgnoreCase) != -1 || + exception.Message.IndexOf("CLIInternalError", StringComparison.OrdinalIgnoreCase) != -1; + + if (isRefreshTokenFailedError) + { + throw new CredentialUnavailableException(InteractiveLoginRequired); + } + + throw new AuthenticationFailedException($"{AzdCliFailedError} {Troubleshoot} {exception.Message}"); + } + catch (Exception ex) + { + throw new CredentialUnavailableException($"{AzdCliFailedError} {Troubleshoot} {ex.Message}"); + } + } + + private async ValueTask RunProcessAsync(ProcessStartInfo processStartInfo, CancellationToken cancellationToken = default) + { + var process = Process.Start(processStartInfo); + if (process == null) + { + throw new CredentialUnavailableException(AzdCliFailedError); + } + + await process.WaitForExitAsync(cancellationToken); + + if (process.ExitCode != 0) + { + var errorMessage = process.StandardError.ReadToEnd(); + throw new InvalidOperationException(errorMessage); + } + + return process.StandardOutput.ReadToEnd(); + } + + private ProcessStartInfo GetAzdCliProcessStartInfo(string[] scopes) + { + string scopeArgs = string.Join(" ", scopes.Select(scope => string.Format($"--scope {scope}"))); + string command = $"azd auth token --output json {scopeArgs}"; + + string fileName; + string argument; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + fileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe"); + argument = $"/c \"{command}\""; + } + else + { + fileName = "/bin/sh"; + argument = $"-c \"{command}\""; + } + + return new ProcessStartInfo + { + FileName = fileName, + Arguments = argument, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + ErrorDialog = false, + CreateNoWindow = true, + WorkingDirectory = DefaultWorkingDir, + }; + } + + private static AccessToken DeserializeOutput(string output) + { + using JsonDocument document = JsonDocument.Parse(output); + + JsonElement root = document.RootElement; + string accessToken = root.GetProperty("token").GetString(); + DateTimeOffset expiresOn = root.GetProperty("expiresOn").GetDateTimeOffset(); + + return new AccessToken(accessToken, expiresOn); + } + } +} diff --git a/src/Web/Program.cs b/src/Web/Program.cs index e1181e2..a998b28 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -1,5 +1,6 @@ using System.Net.Mime; using Ardalis.ListStartupServices; +using Azure.Identity; using BlazorAdmin; using BlazorAdmin.Services; using Blazored.LocalStorage; @@ -8,6 +9,7 @@ using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.Infrastructure.Data; @@ -21,7 +23,19 @@ var builder = WebApplication.CreateBuilder(args); builder.Logging.AddConsole(); -Microsoft.eShopWeb.Infrastructure.Dependencies.ConfigureServices(builder.Configuration, builder.Services); +// Configure SQL Server +var credential = new ChainedTokenCredential(new AzureDeveloperCliCredential(), new DefaultAzureCredential()); +builder.Configuration.AddAzureKeyVault(new Uri(builder.Configuration["AZURE_KEY_VAULT_ENDPOINT"]), credential); +builder.Services.AddDbContext(c => +{ + var connectionString = builder.Configuration[builder.Configuration["AZURE_SQL_CATALOG_CONNECTION_STRING_KEY"]]; + c.UseSqlServer(connectionString, sqlOptions => sqlOptions.EnableRetryOnFailure()); +}); +builder.Services.AddDbContext(options => +{ + var connectionString = builder.Configuration[builder.Configuration["AZURE_SQL_IDENTITY_CONNECTION_STRING_KEY"]]; + options.UseSqlServer(connectionString, sqlOptions => sqlOptions.EnableRetryOnFailure()); +}); builder.Services.AddCookieSettings(); diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index 1d06eb0..6ec695d 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -17,6 +17,8 @@ + +