Add Azure CLI authentication (#10157)

Adds the ability to use an active `az login` session for authenticating the Azure builder
This commit is contained in:
Simon Gottschlag 2020-11-06 20:24:16 +01:00 committed by GitHub
parent bb076d8ad7
commit 65b7d3b604
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 222 additions and 0 deletions

View File

@ -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"
}
}]
}
`

View File

@ -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},

View File

@ -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},

View File

@ -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 {

View File

@ -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"),

View File

@ -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")
}

View File

@ -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())},

1
go.mod
View File

@ -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

View File

@ -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},

View File

@ -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`

View File

@ -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.