From 12707af3f3d0cb4dd8c1073dc335046b9597a939 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Thu, 26 May 2022 13:51:33 +1000 Subject: [PATCH 1/6] Sets supported account types --- source/Sashimi/AzurePowerShellActionHandler.cs | 3 +++ source/Sashimi/Sashimi.csproj | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/source/Sashimi/AzurePowerShellActionHandler.cs b/source/Sashimi/AzurePowerShellActionHandler.cs index bc6d98a..433f78a 100644 --- a/source/Sashimi/AzurePowerShellActionHandler.cs +++ b/source/Sashimi/AzurePowerShellActionHandler.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using Octopus.Server.Extensibility.HostServices.Diagnostics; using Sashimi.Server.Contracts; +using Sashimi.Server.Contracts.Accounts; using Sashimi.Server.Contracts.ActionHandlers; namespace Sashimi.AzureScripting @@ -16,6 +18,7 @@ public class AzurePowerShellActionHandler : IActionHandlerWithAccount public bool CanRunOnDeploymentTarget => false; public ActionHandlerCategory[] Categories => new[] { ActionHandlerCategory.BuiltInStep, AzureConstants.AzureActionHandlerCategory, ActionHandlerCategory.Script }; public string[] StepBasedVariableNameForAccountIds { get; } = { SpecialVariables.Action.Azure.AccountId }; + IEnumerable SupportedAccountTypes => new[] { Sashimi.Azure.Accounts.AccountTypes.AzureServicePrincipalAccountType }; public IActionHandlerResult Execute(IActionHandlerContext context, ITaskLog taskLog) { diff --git a/source/Sashimi/Sashimi.csproj b/source/Sashimi/Sashimi.csproj index ebe5d60..143d103 100644 --- a/source/Sashimi/Sashimi.csproj +++ b/source/Sashimi/Sashimi.csproj @@ -23,7 +23,7 @@ - + From 8185f58264b6c9c3a23a5c33c8f3a17484154dae Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Thu, 26 May 2022 17:37:54 +1000 Subject: [PATCH 2/6] wip authenticating via cloud connections --- source/Calamari/AzureContextScriptWrapper.cs | 166 ++++++++++++++++-- .../AzurePowerShellActionHandlerFixture.cs | 36 ++++ .../AzurePowerShellActionHandlerValidator.cs | 1 + 3 files changed, 189 insertions(+), 14 deletions(-) diff --git a/source/Calamari/AzureContextScriptWrapper.cs b/source/Calamari/AzureContextScriptWrapper.cs index c0c4ea1..577184a 100644 --- a/source/Calamari/AzureContextScriptWrapper.cs +++ b/source/Calamari/AzureContextScriptWrapper.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using Calamari.Common.Features.Discovery; using Calamari.Common.Features.EmbeddedResources; using Calamari.Common.Features.Processes; using Calamari.Common.Features.Scripting; @@ -11,6 +12,7 @@ using Calamari.Common.Plumbing.FileSystem; using Calamari.Common.Plumbing.Logging; using Calamari.Common.Plumbing.Variables; +using Newtonsoft.Json; namespace Calamari.AzureScripting { @@ -49,37 +51,173 @@ public CommandResult ExecuteScript(Script script, variables.Set("OctopusAzureTargetScript", script.File); variables.Set("OctopusAzureTargetScriptParameters", script.Parameters); - SetOutputVariable("OctopusAzureSubscriptionId", variables.Get(SpecialVariables.Action.Azure.SubscriptionId)!); - SetOutputVariable("OctopusAzureStorageAccountName", variables.Get(SpecialVariables.Action.Azure.StorageAccountName)!); - var azureEnvironment = variables.Get(SpecialVariables.Action.Azure.Environment, DefaultAzureEnvironment)!; - if (azureEnvironment != DefaultAzureEnvironment) + var cloudConnectionContext = GetCloudConnectionContext(variables); + AzureContextScriptWrapperAuthentication? scriptAuthentication; + if (cloudConnectionContext != null) { - log.InfoFormat("Using Azure Environment override - {0}", azureEnvironment); + scriptAuthentication = AzureContextScriptWrapperAuthentication.CreateFromCloudConnectionContext(cloudConnectionContext); } - SetOutputVariable("OctopusAzureEnvironment", azureEnvironment); + else + { + scriptAuthentication = AzureContextScriptWrapperAuthentication.CreateFromKnownVariables(variables); + } + + SetOutputVariable("OctopusAzureSubscriptionId", scriptAuthentication.SubscriptionId); + SetOutputVariable("OctopusAzureStorageAccountName", scriptAuthentication.StorageAccountName!); + if (scriptAuthentication.AzureEnvironment != DefaultAzureEnvironment) + { + log.InfoFormat("Using Azure Environment override - {0}", scriptAuthentication.AzureEnvironment!); + } + SetOutputVariable("OctopusAzureEnvironment", scriptAuthentication.AzureEnvironment!); SetOutputVariable("OctopusAzureExtensionsDirectory", variables.Get(SpecialVariables.Action.Azure.ExtensionsDirectory)!); using (new TemporaryFile(Path.Combine(workingDirectory, "AzureProfile.json"))) using (var contextScriptFile = new TemporaryFile(CreateContextScriptFile(workingDirectory, scriptSyntax))) { - if (variables.Get(SpecialVariables.Account.AccountType) == "AzureServicePrincipal") + if (scriptAuthentication.UseServicePrincipal) { SetOutputVariable("OctopusUseServicePrincipal", bool.TrueString); - SetOutputVariable("OctopusAzureADTenantId", variables.Get(SpecialVariables.Action.Azure.TenantId)!); - SetOutputVariable("OctopusAzureADClientId", variables.Get(SpecialVariables.Action.Azure.ClientId)!); - variables.Set("OctopusAzureADPassword", variables.Get(SpecialVariables.Action.Azure.Password)); + SetOutputVariable("OctopusAzureADTenantId", scriptAuthentication.TenantId!); + SetOutputVariable("OctopusAzureADClientId", scriptAuthentication.ClientId!); + variables.Set("OctopusAzureADPassword", scriptAuthentication.Password); return NextWrapper!.ExecuteScript(new Script(contextScriptFile.FilePath), scriptSyntax, commandLineRunner, environmentVars); } //otherwise use management certificate SetOutputVariable("OctopusUseServicePrincipal", false.ToString()); - using (new TemporaryFile(CreateAzureCertificate(workingDirectory))) + using (new TemporaryFile(CreateAzureCertificate(workingDirectory, scriptAuthentication))) { return NextWrapper!.ExecuteScript(new Script(contextScriptFile.FilePath), scriptSyntax, commandLineRunner, environmentVars); } } } + private class CloudConnectionContext + { + public TAuthentication Authentication { get; set; } + + public CloudConnectionContext(TAuthentication authentication) + { + Authentication = authentication; + } + } + + private class AzureCloudConnectionAuthentication + { + public ServicePrincipalAccount Account { get; set; } + + public AzureCloudConnectionAuthentication(ServicePrincipalAccount account) + { + Account = account; + } + } + + class AzureContextScriptWrapperAuthentication + { + public AzureContextScriptWrapperAuthentication( + string subscriptionId, + string azureEnvironment, + string? clientId, + string? tenantId, + string? password, + string? storageAccountName, + string? certificateThumbprint, + byte[]? certificateBytes, + bool useServicePrincipal) + { + SubscriptionId = subscriptionId; + AzureEnvironment = azureEnvironment; + ClientId = clientId; + TenantId = tenantId; + Password = password; + StorageAccountName = storageAccountName; + CertificateThumbprint = certificateThumbprint; + CertificateBytes = certificateBytes; + UseServicePrincipal = useServicePrincipal; + } + + public static AzureContextScriptWrapperAuthentication CreateFromKnownVariables(IVariables variables) + { + return new AzureContextScriptWrapperAuthentication( + subscriptionId: variables.Get(SpecialVariables.Action.Azure.SubscriptionId)!, + azureEnvironment: variables.Get(SpecialVariables.Action.Azure.Environment, DefaultAzureEnvironment)!, + clientId: variables.Get(SpecialVariables.Action.Azure.ClientId), + tenantId: variables.Get(SpecialVariables.Action.Azure.TenantId), + password: variables.Get(SpecialVariables.Action.Azure.Password), + variables.Get(SpecialVariables.Action.Azure.StorageAccountName)!, + variables.Get(SpecialVariables.Action.Azure.CertificateThumbprint), + variables.IsSet(SpecialVariables.Action.Azure.CertificateBytes) ? Convert.FromBase64String(variables.Get(SpecialVariables.Action.Azure.CertificateBytes)!) : null, + variables.Get(SpecialVariables.Account.AccountType) == "AzureServicePrincipal"); + } + + public static AzureContextScriptWrapperAuthentication CreateFromCloudConnectionContext(CloudConnectionContext context) + { + return new AzureContextScriptWrapperAuthentication( + context.Authentication.Account.SubscriptionNumber, + context.Authentication.Account.AzureEnvironment, + context.Authentication.Account.ClientId, + context.Authentication.Account.TenantId, + context.Authentication.Account.Password, + null, + null, + null, + true); + } + + public string SubscriptionId { get; } + public string AzureEnvironment { get; } + public string? ClientId { get; } + public string? TenantId { get; } + public string? Password { get; } + public string? StorageAccountName { get; set; } + public string? CertificateThumbprint { get; set; } + public byte[]? CertificateBytes { get; set; } + public bool UseServicePrincipal { get; set; } + } + + class ServicePrincipalAccount + { + public ServicePrincipalAccount( + string subscriptionNumber, + string clientId, + string tenantId, + string password, + string azureEnvironment) + { + SubscriptionNumber = subscriptionNumber; + ClientId = clientId; + TenantId = tenantId; + Password = password; + AzureEnvironment = azureEnvironment; + } + + public string SubscriptionNumber { get; } + public string ClientId { get; } + public string TenantId { get; } + public string Password { get; } + public string AzureEnvironment { get; } + } + + private CloudConnectionContext? GetCloudConnectionContext(IVariables variables) + { + const string contextVariableName = "Octopus.CloudConnection.Context"; + var json = variables.Get(contextVariableName); + if (json == null) + { + return null; + } + + try + { + return JsonConvert.DeserializeObject>(json); + } + catch (Exception ex) + { + Log.Warn($"Cloud connection context from variable {contextVariableName} is in wrong format: {ex.Message}"); + return null; + } + } + string CreateContextScriptFile(string workingDirectory, ScriptSyntax syntax) { string contextFile; @@ -101,12 +239,12 @@ string CreateContextScriptFile(string workingDirectory, ScriptSyntax syntax) return azureContextScriptFile; } - string CreateAzureCertificate(string workingDirectory) + string CreateAzureCertificate(string workingDirectory, AzureContextScriptWrapperAuthentication scriptAuthentication) { var certificateFilePath = Path.Combine(workingDirectory, CertificateFileName); var certificatePassword = GenerateCertificatePassword(); - var azureCertificate = CalamariCertificateStore.GetOrAdd(variables.Get(SpecialVariables.Action.Azure.CertificateThumbprint)!, - Convert.FromBase64String(variables.Get(SpecialVariables.Action.Azure.CertificateBytes)!), + var azureCertificate = CalamariCertificateStore.GetOrAdd(scriptAuthentication.CertificateThumbprint!, + scriptAuthentication.CertificateBytes!, StoreName.My); variables.Set("OctopusAzureCertificateFileName", certificateFilePath); diff --git a/source/Sashimi.Tests/AzurePowerShellActionHandlerFixture.cs b/source/Sashimi.Tests/AzurePowerShellActionHandlerFixture.cs index 6b899cf..ec61c70 100644 --- a/source/Sashimi.Tests/AzurePowerShellActionHandlerFixture.cs +++ b/source/Sashimi.Tests/AzurePowerShellActionHandlerFixture.cs @@ -69,6 +69,28 @@ public void ExecuteAnInlinePowerShellCoreScript() .Execute(); } + [Test] + [RequiresPowerShell5OrAbove] + public void ExecuteAnInlinePowerShellCoreScriptWithCloudConnection() + { + var psScript = @" +$ErrorActionPreference = 'Continue' +az --version +Get-AzureEnvironment +az group list"; + + ActionHandlerTestBuilder.CreateAsync() + .WithArrange(context => + { + AddCloudConnection(context); + context.Variables.Add(Calamari.Common.Plumbing.Variables.PowerShellVariables.Edition, "Core"); + context.Variables.Add(KnownVariables.Action.Script.ScriptSource, KnownVariableValues.Action.Script.ScriptSource.Inline); + context.Variables.Add(KnownVariables.Action.Script.Syntax, ScriptSyntax.PowerShell.ToString()); + context.Variables.Add(KnownVariables.Action.Script.ScriptBody, psScript); + }) + .Execute(); + } + [Test] [RequiresPowerShell5OrAbove] public void ExecuteAnInlinePowerShellCoreScriptAgainstAnInvalidAzureEnvironment() @@ -100,5 +122,19 @@ void AddDefaults(TestActionHandlerContext context) context.Variables.Add("Octopus.Action.Azure.ClientId", clientId); context.Variables.Add("Octopus.Action.Azure.Password", clientSecret); } + + void AddCloudConnection(TestActionHandlerContext context) + { + context.Variables.Add("Octopus.CloudConnection.Context", @$"{{ + ""authentication"": {{ + ""account"": {{ + ""subscriptionNumber"": ""{subscriptionId}"", + ""tenantId"": ""{tenantId}"", + ""clientId"": ""{clientId}"", + ""password"": ""{clientSecret}"" + }} + }} +}}"); + } } } \ No newline at end of file diff --git a/source/Sashimi/AzurePowerShellActionHandlerValidator.cs b/source/Sashimi/AzurePowerShellActionHandlerValidator.cs index b2ced99..5548761 100644 --- a/source/Sashimi/AzurePowerShellActionHandlerValidator.cs +++ b/source/Sashimi/AzurePowerShellActionHandlerValidator.cs @@ -8,6 +8,7 @@ class AzurePowerShellActionHandlerValidator : AbstractValidator a.ActionType == SpecialVariables.Action.Azure.ActionTypeName, () => { From 89ce17e7ccd9cf65e0d2adb5dd2624770ae99922 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 27 May 2022 13:21:17 +1000 Subject: [PATCH 3/6] Fixes default azure environment --- source/Calamari/AzureContextScriptWrapper.cs | 6 +++--- source/Sashimi/AzurePowerShellActionHandler.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/source/Calamari/AzureContextScriptWrapper.cs b/source/Calamari/AzureContextScriptWrapper.cs index 577184a..6ad7e7a 100644 --- a/source/Calamari/AzureContextScriptWrapper.cs +++ b/source/Calamari/AzureContextScriptWrapper.cs @@ -154,7 +154,7 @@ public static AzureContextScriptWrapperAuthentication CreateFromCloudConnectionC { return new AzureContextScriptWrapperAuthentication( context.Authentication.Account.SubscriptionNumber, - context.Authentication.Account.AzureEnvironment, + context.Authentication.Account.AzureEnvironment ?? DefaultAzureEnvironment, context.Authentication.Account.ClientId, context.Authentication.Account.TenantId, context.Authentication.Account.Password, @@ -182,7 +182,7 @@ public ServicePrincipalAccount( string clientId, string tenantId, string password, - string azureEnvironment) + string? azureEnvironment) { SubscriptionNumber = subscriptionNumber; ClientId = clientId; @@ -195,7 +195,7 @@ public ServicePrincipalAccount( public string ClientId { get; } public string TenantId { get; } public string Password { get; } - public string AzureEnvironment { get; } + public string? AzureEnvironment { get; } } private CloudConnectionContext? GetCloudConnectionContext(IVariables variables) diff --git a/source/Sashimi/AzurePowerShellActionHandler.cs b/source/Sashimi/AzurePowerShellActionHandler.cs index 433f78a..85424f8 100644 --- a/source/Sashimi/AzurePowerShellActionHandler.cs +++ b/source/Sashimi/AzurePowerShellActionHandler.cs @@ -18,7 +18,7 @@ public class AzurePowerShellActionHandler : IActionHandlerWithAccount public bool CanRunOnDeploymentTarget => false; public ActionHandlerCategory[] Categories => new[] { ActionHandlerCategory.BuiltInStep, AzureConstants.AzureActionHandlerCategory, ActionHandlerCategory.Script }; public string[] StepBasedVariableNameForAccountIds { get; } = { SpecialVariables.Action.Azure.AccountId }; - IEnumerable SupportedAccountTypes => new[] { Sashimi.Azure.Accounts.AccountTypes.AzureServicePrincipalAccountType }; + public IEnumerable SupportedAccountTypes => new[] { Sashimi.Azure.Accounts.AccountTypes.AzureServicePrincipalAccountType }; public IActionHandlerResult Execute(IActionHandlerContext context, ITaskLog taskLog) { From 5a65e0c508ff99e5788b800bcf550eddfa6f5253 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 27 May 2022 15:03:31 +1000 Subject: [PATCH 4/6] Renames to AccountDetails to match discovery --- source/Calamari/AzureContextScriptWrapper.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/source/Calamari/AzureContextScriptWrapper.cs b/source/Calamari/AzureContextScriptWrapper.cs index 6ad7e7a..a251dad 100644 --- a/source/Calamari/AzureContextScriptWrapper.cs +++ b/source/Calamari/AzureContextScriptWrapper.cs @@ -104,11 +104,11 @@ public CloudConnectionContext(TAuthentication authentication) private class AzureCloudConnectionAuthentication { - public ServicePrincipalAccount Account { get; set; } + public ServicePrincipalAccount AccountDetails { get; set; } - public AzureCloudConnectionAuthentication(ServicePrincipalAccount account) + public AzureCloudConnectionAuthentication(ServicePrincipalAccount accountDetails) { - Account = account; + AccountDetails = accountDetails; } } @@ -153,11 +153,11 @@ public static AzureContextScriptWrapperAuthentication CreateFromKnownVariables(I public static AzureContextScriptWrapperAuthentication CreateFromCloudConnectionContext(CloudConnectionContext context) { return new AzureContextScriptWrapperAuthentication( - context.Authentication.Account.SubscriptionNumber, - context.Authentication.Account.AzureEnvironment ?? DefaultAzureEnvironment, - context.Authentication.Account.ClientId, - context.Authentication.Account.TenantId, - context.Authentication.Account.Password, + context.Authentication.AccountDetails.SubscriptionNumber, + context.Authentication.AccountDetails.AzureEnvironment ?? DefaultAzureEnvironment, + context.Authentication.AccountDetails.ClientId, + context.Authentication.AccountDetails.TenantId, + context.Authentication.AccountDetails.Password, null, null, null, From a7e4fc5f379aac7e75e3c12e15e5dd3b4017bfc5 Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 27 May 2022 15:21:12 +1000 Subject: [PATCH 5/6] Fixes empty azure environment --- source/Calamari/AzureContextScriptWrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Calamari/AzureContextScriptWrapper.cs b/source/Calamari/AzureContextScriptWrapper.cs index a251dad..60668e0 100644 --- a/source/Calamari/AzureContextScriptWrapper.cs +++ b/source/Calamari/AzureContextScriptWrapper.cs @@ -154,7 +154,7 @@ public static AzureContextScriptWrapperAuthentication CreateFromCloudConnectionC { return new AzureContextScriptWrapperAuthentication( context.Authentication.AccountDetails.SubscriptionNumber, - context.Authentication.AccountDetails.AzureEnvironment ?? DefaultAzureEnvironment, + !string.IsNullOrEmpty(context.Authentication.AccountDetails.AzureEnvironment) ? context.Authentication.AccountDetails.AzureEnvironment! : DefaultAzureEnvironment, context.Authentication.AccountDetails.ClientId, context.Authentication.AccountDetails.TenantId, context.Authentication.AccountDetails.Password, From d2faa303cd1b524d5f1e4ae26b560f0645c8a0cb Mon Sep 17 00:00:00 2001 From: Geoff Lamrock Date: Fri, 27 May 2022 16:24:17 +1000 Subject: [PATCH 6/6] Removes account validation for testing --- .../Sashimi/AzurePowerShellActionHandlerValidator.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/source/Sashimi/AzurePowerShellActionHandlerValidator.cs b/source/Sashimi/AzurePowerShellActionHandlerValidator.cs index 5548761..97292ed 100644 --- a/source/Sashimi/AzurePowerShellActionHandlerValidator.cs +++ b/source/Sashimi/AzurePowerShellActionHandlerValidator.cs @@ -9,12 +9,12 @@ class AzurePowerShellActionHandlerValidator : AbstractValidator a.ActionType == SpecialVariables.Action.Azure.ActionTypeName, - () => - { - RuleFor(a => a.Properties) - .MustHaveProperty(SpecialVariables.Action.Azure.AccountId, "Please select an Account or provide a variable expression for the Account ID to use."); - }); + //When(a => a.ActionType == SpecialVariables.Action.Azure.ActionTypeName, + // () => + // { + // RuleFor(a => a.Properties) + // .MustHaveProperty(SpecialVariables.Action.Azure.AccountId, "Please select an Account or provide a variable expression for the Account ID to use."); + // }); } } } \ No newline at end of file