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:
parent
bb076d8ad7
commit
65b7d3b604
|
@ -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"
|
||||
}
|
||||
}]
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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
1
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
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue