Builder looks up tenant ID before asking for token. Client config did not allow that. Also found that token provider was not properly initialized. Fixes 7267
223 lines
6.6 KiB
Go
223 lines
6.6 KiB
Go
package arm
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Azure/go-autorest/autorest/adal"
|
|
"github.com/Azure/go-autorest/autorest/azure"
|
|
jwt "github.com/dgrijalva/jwt-go"
|
|
"github.com/hashicorp/packer/packer"
|
|
)
|
|
|
|
// ClientConfig allows for various ways to authenticate Azure clients
|
|
type ClientConfig struct {
|
|
// Describes where API's are
|
|
|
|
CloudEnvironmentName string `mapstructure:"cloud_environment_name"`
|
|
cloudEnvironment *azure.Environment
|
|
|
|
// Authentication fields
|
|
|
|
// Client ID
|
|
ClientID string `mapstructure:"client_id"`
|
|
// Client secret/password
|
|
ClientSecret string `mapstructure:"client_secret"`
|
|
// Certificate path for client auth
|
|
ClientCertPath string `mapstructure:"client_cert_path"`
|
|
// JWT bearer token for client auth (RFC 7523, Sec. 2.2)
|
|
ClientJWT string `mapstructure:"client_jwt"`
|
|
ObjectID string `mapstructure:"object_id"`
|
|
TenantID string `mapstructure:"tenant_id"`
|
|
SubscriptionID string `mapstructure:"subscription_id"`
|
|
}
|
|
|
|
const DefaultCloudEnvironmentName = "Public"
|
|
|
|
func (c *ClientConfig) provideDefaultValues() {
|
|
if c.CloudEnvironmentName == "" {
|
|
c.CloudEnvironmentName = DefaultCloudEnvironmentName
|
|
}
|
|
}
|
|
|
|
func (c *ClientConfig) setCloudEnvironment() error {
|
|
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)
|
|
c.cloudEnvironment = &env
|
|
return err
|
|
}
|
|
|
|
func (c ClientConfig) assertRequiredParametersSet(errs *packer.MultiError) {
|
|
/////////////////////////////////////////////
|
|
// 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.
|
|
|
|
if c.useMSI() {
|
|
return
|
|
}
|
|
|
|
if c.useDeviceLogin() {
|
|
return
|
|
}
|
|
|
|
if c.SubscriptionID != "" && c.ClientID != "" &&
|
|
c.ClientSecret != "" &&
|
|
c.ClientCertPath == "" &&
|
|
c.ClientJWT == "" {
|
|
// Service principal using secret
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
errs = packer.MultiErrorAppend(errs, fmt.Errorf("No valid set of authentication values specified:\n"+
|
|
" 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."))
|
|
}
|
|
|
|
func (c ClientConfig) useDeviceLogin() bool {
|
|
return c.SubscriptionID != "" &&
|
|
c.ClientID == "" &&
|
|
c.ClientSecret == "" &&
|
|
c.ClientJWT == "" &&
|
|
c.ClientCertPath == ""
|
|
}
|
|
|
|
func (c ClientConfig) useMSI() bool {
|
|
return c.SubscriptionID == "" &&
|
|
c.ClientID == "" &&
|
|
c.ClientSecret == "" &&
|
|
c.ClientJWT == "" &&
|
|
c.ClientCertPath == "" &&
|
|
c.TenantID == ""
|
|
}
|
|
|
|
func (c ClientConfig) getServicePrincipalTokens(
|
|
say func(string)) (
|
|
servicePrincipalToken *adal.ServicePrincipalToken,
|
|
servicePrincipalTokenVault *adal.ServicePrincipalToken,
|
|
err error) {
|
|
|
|
tenantID := c.TenantID
|
|
|
|
var auth oAuthTokenProvider
|
|
|
|
if c.useDeviceLogin() {
|
|
say("Getting tokens using device flow")
|
|
auth = NewDeviceFlowOAuthTokenProvider(*c.cloudEnvironment, say, tenantID)
|
|
} else if c.useMSI() {
|
|
say("Getting tokens using Managed Identity for Azure")
|
|
auth = NewMSIOAuthTokenProvider(*c.cloudEnvironment)
|
|
} else if c.ClientSecret != "" {
|
|
say("Getting tokens using client secret")
|
|
auth = NewSecretOAuthTokenProvider(*c.cloudEnvironment, c.ClientID, c.ClientSecret, tenantID)
|
|
} else if c.ClientCertPath != "" {
|
|
say("Getting tokens using client certificate")
|
|
auth, err = NewCertOAuthTokenProvider(*c.cloudEnvironment, c.ClientID, c.ClientCertPath, tenantID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
} else {
|
|
say("Getting tokens using client bearer JWT")
|
|
auth = NewJWTOAuthTokenProvider(*c.cloudEnvironment, c.ClientID, c.ClientJWT, tenantID)
|
|
}
|
|
|
|
servicePrincipalToken, err = auth.getServicePrincipalToken()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
err = servicePrincipalToken.EnsureFresh()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
servicePrincipalTokenVault, err = auth.getServicePrincipalTokenWithResource(
|
|
strings.TrimRight(c.cloudEnvironment.KeyVaultEndpoint, "/"))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
err = servicePrincipalTokenVault.EnsureFresh()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return servicePrincipalToken, servicePrincipalTokenVault, nil
|
|
}
|