2019-05-31 08:27:41 -04:00
//go:generate struct-markdown
2019-05-31 18:37:43 -04:00
package client
2019-01-10 18:21:16 -05:00
import (
"fmt"
2019-01-11 15:54:15 -05:00
"os"
2019-01-10 18:21:16 -05:00
"strings"
2019-01-11 15:54:15 -05:00
"time"
2019-01-10 18:21:16 -05:00
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
2019-01-11 15:54:15 -05:00
jwt "github.com/dgrijalva/jwt-go"
2019-01-10 18:21:16 -05:00
"github.com/hashicorp/packer/packer"
)
2019-05-31 18:37:43 -04:00
// Config allows for various ways to authenticate Azure clients.
// When `client_id` and `subscription_id` are specified, Packer will use the
2020-05-14 09:00:27 -04:00
// specified Azure Active Directory (AAD) Service Principal (SP).
2019-05-31 18:37:43 -04:00
// If only `subscription_id` is specified, Packer will try to interactively
// log on the current user (tokens will be cached).
// If none of these options are specified, Packer will attempt to use the
// Managed Identity and subscription of the VM that Packer is running on.
// This will only work if Packer is running on an Azure VM.
type Config struct {
2019-05-28 11:50:58 -04:00
// One of Public, China, Germany, or
2019-06-06 10:29:25 -04:00
// USGovernment. Defaults to Public. Long forms such as
// USGovernmentCloud and AzureUSGovernmentCloud are also supported.
2019-05-28 11:50:58 -04:00
CloudEnvironmentName string ` mapstructure:"cloud_environment_name" required:"false" `
2019-10-14 09:59:26 -04:00
cloudEnvironment * azure . Environment
2019-01-10 18:21:16 -05:00
// Authentication fields
2019-05-31 18:37:43 -04:00
// The application ID of the AAD Service Principal.
// Requires either `client_secret`, `client_cert_path` or `client_jwt` to be set as well.
2019-01-10 18:21:16 -05:00
ClientID string ` mapstructure:"client_id" `
2019-05-31 18:37:43 -04:00
// A password/secret registered for the AAD SP.
2019-01-11 15:54:15 -05:00
ClientSecret string ` mapstructure:"client_secret" `
2019-11-07 04:30:51 -05:00
// The path to a pem-encoded certificate that will be used to authenticate
// as the specified AAD SP.
2019-01-11 15:54:15 -05:00
ClientCertPath string ` mapstructure:"client_cert_path" `
2019-05-31 18:37:43 -04:00
// A JWT bearer token for client auth (RFC 7523, Sec. 2.2) that will be used
// to authenticate the AAD SP. Provides more control over token the expiration
// when using certificate authentication than when using `client_cert_path`.
2019-06-06 10:29:25 -04:00
ClientJWT string ` mapstructure:"client_jwt" `
2019-05-31 18:37:43 -04:00
// The object ID for the AAD SP. Optional, will be derived from the oAuth token if left empty.
ObjectID string ` mapstructure:"object_id" `
// The Active Directory tenant identifier with which your `client_id` and
// `subscription_id` are associated. If not specified, `tenant_id` will be
// looked up using `subscription_id`.
TenantID string ` mapstructure:"tenant_id" required:"false" `
// The subscription to use.
2019-01-10 18:21:16 -05:00
SubscriptionID string ` mapstructure:"subscription_id" `
2019-05-31 18:37:43 -04:00
authType string
2020-11-06 14:24:16 -05:00
// 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" `
2019-01-10 18:21:16 -05:00
}
2019-05-31 18:37:43 -04:00
const (
authTypeDeviceLogin = "DeviceLogin"
authTypeMSI = "ManagedIdentity"
authTypeClientSecret = "ClientSecret"
authTypeClientCert = "ClientCertificate"
authTypeClientBearerJWT = "ClientBearerJWT"
2020-11-06 14:24:16 -05:00
authTypeAzureCLI = "AzureCLI"
2019-05-31 18:37:43 -04:00
)
2019-01-10 18:21:16 -05:00
const DefaultCloudEnvironmentName = "Public"
2019-05-31 18:37:43 -04:00
func ( c * Config ) SetDefaultValues ( ) error {
2019-01-10 18:21:16 -05:00
if c . CloudEnvironmentName == "" {
c . CloudEnvironmentName = DefaultCloudEnvironmentName
}
2019-05-31 18:37:43 -04:00
return c . setCloudEnvironment ( )
2019-01-10 18:21:16 -05:00
}
2019-10-14 09:59:26 -04:00
func ( c * Config ) CloudEnvironment ( ) * azure . Environment {
return c . cloudEnvironment
}
2019-05-31 18:37:43 -04:00
func ( c * Config ) setCloudEnvironment ( ) error {
2019-01-10 18:21:16 -05:00
lookup := map [ string ] string {
"CHINA" : "AzureChinaCloud" ,
"CHINACLOUD" : "AzureChinaCloud" ,
"AZURECHINACLOUD" : "AzureChinaCloud" ,
"GERMAN" : "AzureGermanCloud" ,
"GERMANCLOUD" : "AzureGermanCloud" ,
"AZUREGERMANCLOUD" : "AzureGermanCloud" ,
"GERMANY" : "AzureGermanCloud" ,
"GERMANYCLOUD" : "AzureGermanCloud" ,
"AZUREGERMANYCLOUD" : "AzureGermanCloud" ,
"PUBLIC" : "AzurePublicCloud" ,
"PUBLICCLOUD" : "AzurePublicCloud" ,
"AZUREPUBLICCLOUD" : "AzurePublicCloud" ,
"USGOVERNMENT" : "AzureUSGovernmentCloud" ,
"USGOVERNMENTCLOUD" : "AzureUSGovernmentCloud" ,
"AZUREUSGOVERNMENTCLOUD" : "AzureUSGovernmentCloud" ,
}
name := strings . ToUpper ( c . CloudEnvironmentName )
envName , ok := lookup [ name ]
if ! ok {
return fmt . Errorf ( "There is no cloud environment matching the name '%s'!" , c . CloudEnvironmentName )
}
env , err := azure . EnvironmentFromName ( envName )
2019-10-14 09:59:26 -04:00
c . cloudEnvironment = & env
2019-01-10 18:21:16 -05:00
return err
}
2019-05-31 18:37:43 -04:00
func ( c Config ) Validate ( errs * packer . MultiError ) {
2019-01-10 18:21:16 -05:00
/////////////////////////////////////////////
// Authentication via OAUTH
// Check if device login is being asked for, and is allowed.
//
// Device login is enabled if the user only defines SubscriptionID and not
// ClientID, ClientSecret, and TenantID.
//
// Device login is not enabled for Windows because the WinRM certificate is
// 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.
2020-11-06 14:24:16 -05:00
if c . UseCLI ( ) {
return
}
2019-05-31 18:37:43 -04:00
if c . UseMSI ( ) {
2019-01-10 18:21:16 -05:00
return
}
if c . useDeviceLogin ( ) {
return
}
2019-01-11 15:54:15 -05:00
if c . SubscriptionID != "" && c . ClientID != "" &&
c . ClientSecret != "" &&
c . ClientCertPath == "" &&
c . ClientJWT == "" {
2019-01-10 18:21:16 -05:00
// Service principal using secret
return
}
2019-01-11 15:54:15 -05:00
if c . SubscriptionID != "" && c . ClientID != "" &&
c . ClientSecret == "" &&
c . ClientCertPath != "" &&
c . ClientJWT == "" {
// Service principal using certificate
if _ , err := os . Stat ( c . ClientCertPath ) ; err != nil {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "client_cert_path is not an accessible file: %v" , err ) )
}
return
}
if c . SubscriptionID != "" && c . ClientID != "" &&
c . ClientSecret == "" &&
c . ClientCertPath == "" &&
c . ClientJWT != "" {
// Service principal using JWT
// Check that JWT is valid for at least 5 more minutes
p := jwt . Parser { }
claims := jwt . StandardClaims { }
token , _ , err := p . ParseUnverified ( c . ClientJWT , & claims )
if err != nil {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "client_jwt is not a JWT: %v" , err ) )
} else {
if claims . ExpiresAt < time . Now ( ) . Add ( 5 * time . Minute ) . Unix ( ) {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "client_jwt will expire within 5 minutes, please use a JWT that is valid for at least 5 minutes" ) )
}
if t , ok := token . Header [ "x5t" ] ; ! ok || t == "" {
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "client_jwt is missing the x5t header value, which is required for bearer JWT client authentication to Azure" ) )
}
}
return
}
2019-01-10 18:21:16 -05:00
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "No valid set of authentication values specified:\n" +
2019-01-11 15:54:15 -05:00
" to use the Managed Identity of the current machine, do not specify any of the fields below\n" +
" to use interactive user authentication, specify only subscription_id\n" +
" to use an Azure Active Directory service principal, specify either:\n" +
" - subscription_id, client_id and client_secret\n" +
" - subscription_id, client_id and client_cert_path\n" +
" - subscription_id, client_id and client_jwt." ) )
2019-01-10 18:21:16 -05:00
}
2019-05-31 18:37:43 -04:00
func ( c Config ) useDeviceLogin ( ) bool {
2019-01-10 18:21:16 -05:00
return c . SubscriptionID != "" &&
c . ClientID == "" &&
c . ClientSecret == "" &&
2019-01-11 15:54:15 -05:00
c . ClientJWT == "" &&
2019-02-04 18:10:07 -05:00
c . ClientCertPath == ""
2019-01-10 18:21:16 -05:00
}
2020-11-06 14:24:16 -05:00
func ( c Config ) UseCLI ( ) bool {
return c . UseAzureCLIAuth == true
}
2019-05-31 18:37:43 -04:00
func ( c Config ) UseMSI ( ) bool {
2019-01-10 18:21:16 -05:00
return c . SubscriptionID == "" &&
c . ClientID == "" &&
c . ClientSecret == "" &&
2019-01-11 15:54:15 -05:00
c . ClientJWT == "" &&
c . ClientCertPath == "" &&
2019-01-10 18:21:16 -05:00
c . TenantID == ""
}
2019-09-25 17:12:33 -04:00
func ( c Config ) GetServicePrincipalTokens ( say func ( string ) ) (
2019-01-10 18:21:16 -05:00
servicePrincipalToken * adal . ServicePrincipalToken ,
servicePrincipalTokenVault * adal . ServicePrincipalToken ,
err error ) {
2019-09-25 17:12:33 -04:00
servicePrincipalToken , err = c . GetServicePrincipalToken ( say ,
2019-10-16 04:40:25 -04:00
c . CloudEnvironment ( ) . ResourceManagerEndpoint )
2019-09-25 17:12:33 -04:00
if err != nil {
return nil , nil , err
}
servicePrincipalTokenVault , err = c . GetServicePrincipalToken ( say ,
2019-10-16 04:40:25 -04:00
strings . TrimRight ( c . CloudEnvironment ( ) . KeyVaultEndpoint , "/" ) )
2019-09-25 17:12:33 -04:00
if err != nil {
return nil , nil , err
}
return servicePrincipalToken , servicePrincipalTokenVault , nil
}
func ( c Config ) GetServicePrincipalToken (
say func ( string ) , forResource string ) (
servicePrincipalToken * adal . ServicePrincipalToken ,
err error ) {
2019-01-10 18:21:16 -05:00
var auth oAuthTokenProvider
2019-05-31 18:37:43 -04:00
switch c . authType {
case authTypeDeviceLogin :
2019-01-10 18:21:16 -05:00
say ( "Getting tokens using device flow" )
2019-10-16 04:22:56 -04:00
auth = NewDeviceFlowOAuthTokenProvider ( * c . cloudEnvironment , say , c . TenantID )
2020-11-06 14:24:16 -05:00
case authTypeAzureCLI :
say ( "Getting tokens using Azure CLI" )
auth = NewCliOAuthTokenProvider ( * c . cloudEnvironment , say , c . TenantID )
2019-05-31 18:37:43 -04:00
case authTypeMSI :
2019-01-10 18:21:16 -05:00
say ( "Getting tokens using Managed Identity for Azure" )
2019-10-14 09:59:26 -04:00
auth = NewMSIOAuthTokenProvider ( * c . cloudEnvironment )
2019-05-31 18:37:43 -04:00
case authTypeClientSecret :
2019-01-10 18:21:16 -05:00
say ( "Getting tokens using client secret" )
2019-10-16 04:22:56 -04:00
auth = NewSecretOAuthTokenProvider ( * c . cloudEnvironment , c . ClientID , c . ClientSecret , c . TenantID )
2019-05-31 18:37:43 -04:00
case authTypeClientCert :
2019-01-11 15:54:15 -05:00
say ( "Getting tokens using client certificate" )
2019-10-16 04:22:56 -04:00
auth , err = NewCertOAuthTokenProvider ( * c . cloudEnvironment , c . ClientID , c . ClientCertPath , c . TenantID )
2019-01-11 15:54:15 -05:00
if err != nil {
2019-09-25 17:12:33 -04:00
return nil , err
2019-01-11 15:54:15 -05:00
}
2019-05-31 18:37:43 -04:00
case authTypeClientBearerJWT :
2019-01-11 15:54:15 -05:00
say ( "Getting tokens using client bearer JWT" )
2019-10-16 04:22:56 -04:00
auth = NewJWTOAuthTokenProvider ( * c . cloudEnvironment , c . ClientID , c . ClientJWT , c . TenantID )
2019-05-31 18:37:43 -04:00
default :
panic ( "authType not set, call FillParameters, or set explicitly" )
2019-01-10 18:21:16 -05:00
}
2019-09-25 17:12:33 -04:00
servicePrincipalToken , err = auth . getServicePrincipalTokenWithResource ( forResource )
2019-01-10 18:21:16 -05:00
if err != nil {
2019-09-25 17:12:33 -04:00
return nil , err
2019-01-10 18:21:16 -05:00
}
err = servicePrincipalToken . EnsureFresh ( )
if err != nil {
2019-09-25 17:12:33 -04:00
return nil , err
2019-01-10 18:21:16 -05:00
}
2019-09-25 17:12:33 -04:00
return servicePrincipalToken , nil
2019-01-10 18:21:16 -05:00
}
2019-05-31 18:37:43 -04:00
2019-09-25 13:16:17 -04:00
// FillParameters capture the user intent from the supplied parameter set in authType, retrieves the TenantID and CloudEnvironment if not specified.
// The SubscriptionID is also retrieved in case MSI auth is requested.
2019-05-31 18:37:43 -04:00
func ( c * Config ) FillParameters ( ) error {
if c . authType == "" {
if c . useDeviceLogin ( ) {
c . authType = authTypeDeviceLogin
2020-11-06 14:24:16 -05:00
} else if c . UseCLI ( ) {
c . authType = authTypeAzureCLI
2019-05-31 18:37:43 -04:00
} else if c . UseMSI ( ) {
c . authType = authTypeMSI
} else if c . ClientSecret != "" {
c . authType = authTypeClientSecret
} else if c . ClientCertPath != "" {
c . authType = authTypeClientCert
} else {
c . authType = authTypeClientBearerJWT
}
}
if c . authType == authTypeMSI && c . SubscriptionID == "" {
subscriptionID , err := getSubscriptionFromIMDS ( )
if err != nil {
return fmt . Errorf ( "error fetching subscriptionID from VM metadata service for Managed Identity authentication: %v" , err )
}
c . SubscriptionID = subscriptionID
}
2019-10-14 09:59:26 -04:00
if c . cloudEnvironment == nil {
2019-09-25 13:30:58 -04:00
err := c . setCloudEnvironment ( )
2019-05-31 18:37:43 -04:00
if err != nil {
return err
}
}
2020-11-06 14:24:16 -05:00
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
}
2019-09-25 13:30:58 -04:00
if c . TenantID == "" {
2019-10-14 09:59:26 -04:00
tenantID , err := findTenantID ( * c . cloudEnvironment , c . SubscriptionID )
2019-05-31 18:37:43 -04:00
if err != nil {
return err
}
2019-09-25 13:30:58 -04:00
c . TenantID = tenantID
2019-05-31 18:37:43 -04:00
}
return nil
}
// allow override for unit tests
2019-10-07 14:56:20 -04:00
var findTenantID = FindTenantID