diff --git a/builder/azure/arm/builder_acc_test.go b/builder/azure/arm/builder_acc_test.go index 1837bf5b0..42e518cf4 100644 --- a/builder/azure/arm/builder_acc_test.go +++ b/builder/azure/arm/builder_acc_test.go @@ -90,6 +90,19 @@ func TestBuilderAcc_ManagedDisk_Linux_DeviceLogin(t *testing.T) { }) } +func TestBuilderAcc_ManagedDisk_Linux_AzureCLI(t *testing.T) { + if os.Getenv("AZURE_CLI_AUTH") == "" { + t.Skip("Azure CLI Acceptance tests skipped unless env 'AZURE_CLI_AUTH' is set, and an active `az login` session has been established") + return + } + + builderT.Test(t, builderT.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Builder: &Builder{}, + Template: testBuilderAccManagedDiskLinuxAzureCLI, + }) +} + func TestBuilderAcc_Blob_Windows(t *testing.T) { builderT.Test(t, builderT.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -366,3 +379,27 @@ const testBuilderAccBlobLinux = ` }] } ` +const testBuilderAccManagedDiskLinuxAzureCLI = ` +{ + "builders": [{ + "type": "test", + + "use_azure_cli_auth": true, + + "managed_image_resource_group_name": "packer-acceptance-test", + "managed_image_name": "testBuilderAccManagedDiskLinuxAzureCLI-{{timestamp}}", + + "os_type": "Linux", + "image_publisher": "Canonical", + "image_offer": "UbuntuServer", + "image_sku": "16.04-LTS", + + "location": "South Central US", + "vm_size": "Standard_DS2_v2", + "azure_tags": { + "env": "testing", + "builder": "packer" + } + }] +} +` diff --git a/builder/azure/arm/config.hcl2spec.go b/builder/azure/arm/config.hcl2spec.go index 8c7241f3f..5da288787 100644 --- a/builder/azure/arm/config.hcl2spec.go +++ b/builder/azure/arm/config.hcl2spec.go @@ -25,6 +25,7 @@ type FlatConfig struct { ObjectID *string `mapstructure:"object_id" cty:"object_id" hcl:"object_id"` TenantID *string `mapstructure:"tenant_id" required:"false" cty:"tenant_id" hcl:"tenant_id"` SubscriptionID *string `mapstructure:"subscription_id" cty:"subscription_id" hcl:"subscription_id"` + UseAzureCLIAuth *bool `mapstructure:"use_azure_cli_auth" required:"false" cty:"use_azure_cli_auth" hcl:"use_azure_cli_auth"` UserAssignedManagedIdentities []string `mapstructure:"user_assigned_managed_identities" required:"false" cty:"user_assigned_managed_identities" hcl:"user_assigned_managed_identities"` CaptureNamePrefix *string `mapstructure:"capture_name_prefix" cty:"capture_name_prefix" hcl:"capture_name_prefix"` CaptureContainerName *string `mapstructure:"capture_container_name" cty:"capture_container_name" hcl:"capture_container_name"` @@ -151,6 +152,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "object_id": &hcldec.AttrSpec{Name: "object_id", Type: cty.String, Required: false}, "tenant_id": &hcldec.AttrSpec{Name: "tenant_id", Type: cty.String, Required: false}, "subscription_id": &hcldec.AttrSpec{Name: "subscription_id", Type: cty.String, Required: false}, + "use_azure_cli_auth": &hcldec.AttrSpec{Name: "use_azure_cli_auth", Type: cty.Bool, Required: false}, "user_assigned_managed_identities": &hcldec.AttrSpec{Name: "user_assigned_managed_identities", Type: cty.List(cty.String), Required: false}, "capture_name_prefix": &hcldec.AttrSpec{Name: "capture_name_prefix", Type: cty.String, Required: false}, "capture_container_name": &hcldec.AttrSpec{Name: "capture_container_name", Type: cty.String, Required: false}, diff --git a/builder/azure/chroot/builder.hcl2spec.go b/builder/azure/chroot/builder.hcl2spec.go index 059f4f6b6..03bea01cc 100644 --- a/builder/azure/chroot/builder.hcl2spec.go +++ b/builder/azure/chroot/builder.hcl2spec.go @@ -24,6 +24,7 @@ type FlatConfig struct { ObjectID *string `mapstructure:"object_id" cty:"object_id" hcl:"object_id"` TenantID *string `mapstructure:"tenant_id" required:"false" cty:"tenant_id" hcl:"tenant_id"` SubscriptionID *string `mapstructure:"subscription_id" cty:"subscription_id" hcl:"subscription_id"` + UseAzureCLIAuth *bool `mapstructure:"use_azure_cli_auth" required:"false" cty:"use_azure_cli_auth" hcl:"use_azure_cli_auth"` FromScratch *bool `mapstructure:"from_scratch" cty:"from_scratch" hcl:"from_scratch"` Source *string `mapstructure:"source" required:"true" cty:"source" hcl:"source"` CommandWrapper *string `mapstructure:"command_wrapper" cty:"command_wrapper" hcl:"command_wrapper"` @@ -76,6 +77,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "object_id": &hcldec.AttrSpec{Name: "object_id", Type: cty.String, Required: false}, "tenant_id": &hcldec.AttrSpec{Name: "tenant_id", Type: cty.String, Required: false}, "subscription_id": &hcldec.AttrSpec{Name: "subscription_id", Type: cty.String, Required: false}, + "use_azure_cli_auth": &hcldec.AttrSpec{Name: "use_azure_cli_auth", Type: cty.Bool, Required: false}, "from_scratch": &hcldec.AttrSpec{Name: "from_scratch", Type: cty.Bool, Required: false}, "source": &hcldec.AttrSpec{Name: "source", Type: cty.String, Required: false}, "command_wrapper": &hcldec.AttrSpec{Name: "command_wrapper", Type: cty.String, Required: false}, diff --git a/builder/azure/common/client/config.go b/builder/azure/common/client/config.go index 5eb1d8a38..5ee5e6c31 100644 --- a/builder/azure/common/client/config.go +++ b/builder/azure/common/client/config.go @@ -54,6 +54,14 @@ type Config struct { SubscriptionID string `mapstructure:"subscription_id"` authType string + + // Flag to use Azure CLI authentication. Defaults to false. + // CLI auth will use the information from an active `az login` session to connect to Azure and set the subscription id and tenant id associated to the signed in account. + // If enabled, it will use the authentication provided by the `az` CLI. + // Azure CLI authentication will use the credential marked as `isDefault` and can be verified using `az account show`. + // Works with normal authentication (`az login`) and service principals (`az login --service-principal --username APP_ID --password PASSWORD --tenant TENANT_ID`). + // Ignores all other configurations if enabled. + UseAzureCLIAuth bool `mapstructure:"use_azure_cli_auth" required:"false"` } const ( @@ -62,6 +70,7 @@ const ( authTypeClientSecret = "ClientSecret" authTypeClientCert = "ClientCertificate" authTypeClientBearerJWT = "ClientBearerJWT" + authTypeAzureCLI = "AzureCLI" ) const DefaultCloudEnvironmentName = "Public" @@ -124,6 +133,10 @@ func (c Config) Validate(errs *packer.MultiError) { // readable by the ObjectID of the App. There may be another way to handle // this case, but I am not currently aware of it - send feedback. + if c.UseCLI() { + return + } + if c.UseMSI() { return } @@ -193,6 +206,10 @@ func (c Config) useDeviceLogin() bool { c.ClientCertPath == "" } +func (c Config) UseCLI() bool { + return c.UseAzureCLIAuth == true +} + func (c Config) UseMSI() bool { return c.SubscriptionID == "" && c.ClientID == "" && @@ -230,6 +247,9 @@ func (c Config) GetServicePrincipalToken( case authTypeDeviceLogin: say("Getting tokens using device flow") auth = NewDeviceFlowOAuthTokenProvider(*c.cloudEnvironment, say, c.TenantID) + case authTypeAzureCLI: + say("Getting tokens using Azure CLI") + auth = NewCliOAuthTokenProvider(*c.cloudEnvironment, say, c.TenantID) case authTypeMSI: say("Getting tokens using Managed Identity for Azure") auth = NewMSIOAuthTokenProvider(*c.cloudEnvironment) @@ -268,6 +288,8 @@ func (c *Config) FillParameters() error { if c.authType == "" { if c.useDeviceLogin() { c.authType = authTypeDeviceLogin + } else if c.UseCLI() { + c.authType = authTypeAzureCLI } else if c.UseMSI() { c.authType = authTypeMSI } else if c.ClientSecret != "" { @@ -295,6 +317,16 @@ func (c *Config) FillParameters() error { } } + if c.authType == authTypeAzureCLI { + tenantID, subscriptionID, err := getIDsFromAzureCLI() + if err != nil { + return fmt.Errorf("error fetching tenantID and subscriptionID from Azure CLI (are you logged on using `az login`?): %v", err) + } + + c.TenantID = tenantID + c.SubscriptionID = subscriptionID + } + if c.TenantID == "" { tenantID, err := findTenantID(*c.cloudEnvironment, c.SubscriptionID) if err != nil { diff --git a/builder/azure/common/client/config_test.go b/builder/azure/common/client/config_test.go index a914e25c3..8f399b886 100644 --- a/builder/azure/common/client/config_test.go +++ b/builder/azure/common/client/config_test.go @@ -29,6 +29,13 @@ func Test_ClientConfig_RequiredParametersSet(t *testing.T) { config: Config{}, wantErr: false, }, + { + name: "use_azure_cli_auth will trigger Azure CLI auth", + config: Config{ + UseAzureCLIAuth: true, + }, + wantErr: false, + }, { name: "subscription_id is set will trigger device flow", config: Config{ @@ -158,6 +165,26 @@ func Test_ClientConfig_DeviceLogin(t *testing.T) { } } +func Test_ClientConfig_AzureCli(t *testing.T) { + // Azure CLI tests skipped unless env 'AZURE_CLI_AUTH' is set, and an active `az login` session has been established + getEnvOrSkip(t, "AZURE_CLI_AUTH") + + cfg := Config{ + UseAzureCLIAuth: true, + cloudEnvironment: getCloud(), + } + assertValid(t, cfg) + + err := cfg.FillParameters() + if err != nil { + t.Fatalf("Expected nil err, but got: %v", err) + } + + if cfg.authType != authTypeAzureCLI { + t.Fatalf("Expected authType to be %q, but got: %q", authTypeAzureCLI, cfg.authType) + } +} + func Test_ClientConfig_ClientPassword(t *testing.T) { cfg := Config{ SubscriptionID: getEnvOrSkip(t, "AZURE_SUBSCRIPTION"), diff --git a/builder/azure/common/client/tokenprovider_cli.go b/builder/azure/common/client/tokenprovider_cli.go new file mode 100644 index 000000000..b11182e20 --- /dev/null +++ b/builder/azure/common/client/tokenprovider_cli.go @@ -0,0 +1,99 @@ +package client + +import ( + "context" + "errors" + "fmt" + + "github.com/Azure/go-autorest/autorest/adal" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/azure/cli" +) + +// for managed identity auth +type cliOAuthTokenProvider struct { + env azure.Environment + say func(string) + tenantID string +} + +func NewCliOAuthTokenProvider(env azure.Environment, say func(string), tenantID string) oAuthTokenProvider { + return &cliOAuthTokenProvider{ + env: env, + say: say, + tenantID: tenantID, + } +} + +func (tp *cliOAuthTokenProvider) getServicePrincipalToken() (*adal.ServicePrincipalToken, error) { + return tp.getServicePrincipalTokenWithResource(tp.env.ResourceManagerEndpoint) +} + +func (tp *cliOAuthTokenProvider) getServicePrincipalTokenWithResource(resource string) (*adal.ServicePrincipalToken, error) { + token, err := cli.GetTokenFromCLI(resource) + if err != nil { + tp.say(fmt.Sprintf("unable to get token from azure cli: %v", err)) + return nil, err + } + + oAuthConfig, err := adal.NewOAuthConfig(resource, tp.tenantID) + if err != nil { + tp.say(fmt.Sprintf("unable to generate OAuth Config: %v", err)) + return nil, err + } + + adalToken, err := token.ToADALToken() + if err != nil { + tp.say(fmt.Sprintf("unable to get ADAL Token from azure cli token: %v", err)) + return nil, err + } + + spt, err := adal.NewServicePrincipalTokenFromManualToken(*oAuthConfig, clientIDs[tp.env.Name], resource, adalToken) + if err != nil { + tp.say(fmt.Sprintf("unable to get service principal token from adal token: %v", err)) + return nil, err + } + + // Custom refresh function to make it possible to use Azure CLI to refresh tokens. + // Inspired by HashiCorps go-azure-helpers: https://github.com/hashicorp/go-azure-helpers/blob/373622ce2effb0cf299051ea019cb657f357a4d8/authentication/auth_method_azure_cli_token.go#L96-L109 + var customRefreshFunc adal.TokenRefresh = func(ctx context.Context, resource string) (*adal.Token, error) { + token, err := cli.GetTokenFromCLI(resource) + if err != nil { + tp.say(fmt.Sprintf("token refresh - unable to get token from azure cli: %v", err)) + return nil, err + } + + adalToken, err := token.ToADALToken() + if err != nil { + tp.say(fmt.Sprintf("token refresh - unable to get ADAL Token from azure cli token: %v", err)) + return nil, err + } + + return &adalToken, nil + } + + spt.SetCustomRefreshFunc(customRefreshFunc) + + return spt, nil +} + +// getIDsFromAzureCLI returns the TenantID and SubscriptionID from an active Azure CLI login session +func getIDsFromAzureCLI() (string, string, error) { + profilePath, err := cli.ProfilePath() + if err != nil { + return "", "", err + } + + profile, err := cli.LoadProfile(profilePath) + if err != nil { + return "", "", err + } + + for _, p := range profile.Subscriptions { + if p.IsDefault { + return p.TenantID, p.ID, nil + } + } + + return "", "", errors.New("Unable to find default subscription") +} diff --git a/builder/azure/dtl/config.hcl2spec.go b/builder/azure/dtl/config.hcl2spec.go index f1c417062..be321e92a 100644 --- a/builder/azure/dtl/config.hcl2spec.go +++ b/builder/azure/dtl/config.hcl2spec.go @@ -51,6 +51,7 @@ type FlatConfig struct { ObjectID *string `mapstructure:"object_id" cty:"object_id" hcl:"object_id"` TenantID *string `mapstructure:"tenant_id" required:"false" cty:"tenant_id" hcl:"tenant_id"` SubscriptionID *string `mapstructure:"subscription_id" cty:"subscription_id" hcl:"subscription_id"` + UseAzureCLIAuth *bool `mapstructure:"use_azure_cli_auth" required:"false" cty:"use_azure_cli_auth" hcl:"use_azure_cli_auth"` CaptureNamePrefix *string `mapstructure:"capture_name_prefix" cty:"capture_name_prefix" hcl:"capture_name_prefix"` CaptureContainerName *string `mapstructure:"capture_container_name" cty:"capture_container_name" hcl:"capture_container_name"` SharedGallery *FlatSharedImageGallery `mapstructure:"shared_image_gallery" cty:"shared_image_gallery" hcl:"shared_image_gallery"` @@ -163,6 +164,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "object_id": &hcldec.AttrSpec{Name: "object_id", Type: cty.String, Required: false}, "tenant_id": &hcldec.AttrSpec{Name: "tenant_id", Type: cty.String, Required: false}, "subscription_id": &hcldec.AttrSpec{Name: "subscription_id", Type: cty.String, Required: false}, + "use_azure_cli_auth": &hcldec.AttrSpec{Name: "use_azure_cli_auth", Type: cty.Bool, Required: false}, "capture_name_prefix": &hcldec.AttrSpec{Name: "capture_name_prefix", Type: cty.String, Required: false}, "capture_container_name": &hcldec.AttrSpec{Name: "capture_container_name", Type: cty.String, Required: false}, "shared_image_gallery": &hcldec.BlockSpec{TypeName: "shared_image_gallery", Nested: hcldec.ObjectSpec((*FlatSharedImageGallery)(nil).HCL2Spec())}, diff --git a/go.mod b/go.mod index c53e9c9d8..a89a5bc24 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/Azure/go-autorest/autorest v0.10.0 github.com/Azure/go-autorest/autorest/adal v0.8.2 github.com/Azure/go-autorest/autorest/azure/auth v0.4.2 + github.com/Azure/go-autorest/autorest/azure/cli v0.3.1 github.com/Azure/go-autorest/autorest/date v0.2.0 github.com/Azure/go-autorest/autorest/to v0.3.0 github.com/Azure/go-autorest/autorest/validation v0.2.0 // indirect diff --git a/provisioner/azure-dtlartifact/provisioner.hcl2spec.go b/provisioner/azure-dtlartifact/provisioner.hcl2spec.go index d41dec53e..65a4d885d 100644 --- a/provisioner/azure-dtlartifact/provisioner.hcl2spec.go +++ b/provisioner/azure-dtlartifact/provisioner.hcl2spec.go @@ -51,6 +51,7 @@ type FlatConfig struct { ObjectID *string `mapstructure:"object_id" cty:"object_id" hcl:"object_id"` TenantID *string `mapstructure:"tenant_id" required:"false" cty:"tenant_id" hcl:"tenant_id"` SubscriptionID *string `mapstructure:"subscription_id" cty:"subscription_id" hcl:"subscription_id"` + UseAzureCLIAuth *bool `mapstructure:"use_azure_cli_auth" required:"false" cty:"use_azure_cli_auth" hcl:"use_azure_cli_auth"` DtlArtifacts []FlatDtlArtifact `mapstructure:"dtl_artifacts" cty:"dtl_artifacts" hcl:"dtl_artifacts"` LabName *string `mapstructure:"lab_name" cty:"lab_name" hcl:"lab_name"` ResourceGroupName *string `mapstructure:"lab_resource_group_name" cty:"lab_resource_group_name" hcl:"lab_resource_group_name"` @@ -87,6 +88,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "object_id": &hcldec.AttrSpec{Name: "object_id", Type: cty.String, Required: false}, "tenant_id": &hcldec.AttrSpec{Name: "tenant_id", Type: cty.String, Required: false}, "subscription_id": &hcldec.AttrSpec{Name: "subscription_id", Type: cty.String, Required: false}, + "use_azure_cli_auth": &hcldec.AttrSpec{Name: "use_azure_cli_auth", Type: cty.Bool, Required: false}, "dtl_artifacts": &hcldec.BlockListSpec{TypeName: "dtl_artifacts", Nested: hcldec.ObjectSpec((*FlatDtlArtifact)(nil).HCL2Spec())}, "lab_name": &hcldec.AttrSpec{Name: "lab_name", Type: cty.String, Required: false}, "lab_resource_group_name": &hcldec.AttrSpec{Name: "lab_resource_group_name", Type: cty.String, Required: false}, diff --git a/website/pages/docs/builders/azure/index.mdx b/website/pages/docs/builders/azure/index.mdx index fc76e1b89..50b62be43 100644 --- a/website/pages/docs/builders/azure/index.mdx +++ b/website/pages/docs/builders/azure/index.mdx @@ -40,6 +40,7 @@ following methods are available and are explained below: for the Public and US Gov clouds only. - Azure Managed Identity - Azure Active Directory Service Principal +- Azure CLI -> **Don't know which authentication method to use?** Go with interactive login to try out the builders. If you need packer to run automatically, @@ -103,3 +104,13 @@ way to authenticate the SP to AAD: To create a service principal, you can follow [the Azure documentation on this subject](https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli?view=azure-cli-latest). + +## Azure CLI + +This method will skip all other options provided and only use the credentials that the az cli is authenticated with. +Works with both normal user (`az login`) as well as service principal (`az login --service-principal --username APP_ID --password PASSWORD --tenant TENANT_ID`). + +To enable az cli authentication, use the following: +- `"use_azure_cli_auth": true` + +This mode will use the `tenant_id` and `subscription_id` from the current active az session which can be found by running: `az account show` diff --git a/website/pages/partials/builder/azure/common/client/Config-not-required.mdx b/website/pages/partials/builder/azure/common/client/Config-not-required.mdx index c4c6b20eb..46d553c81 100644 --- a/website/pages/partials/builder/azure/common/client/Config-not-required.mdx +++ b/website/pages/partials/builder/azure/common/client/Config-not-required.mdx @@ -23,3 +23,10 @@ looked up using `subscription_id`. - `subscription_id` (string) - The subscription to use. + +- `use_azure_cli_auth` (bool) - Flag to use Azure CLI authentication. Defaults to false. + CLI auth will use the information from an active `az login` session to connect to Azure and set the subscription id and tenant id associated to the signed in account. + If enabled, it will use the authentication provided by the `az` CLI. + Azure CLI authentication will use the credential marked as `isDefault` and can be verified using `az account show`. + Works with normal authentication (`az login`) and service principals (`az login --service-principal --username APP_ID --password PASSWORD --tenant TENANT_ID`). + Ignores all other configurations if enabled.