224 lines
6.7 KiB
Go
224 lines
6.7 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 == "" &&
|
|
c.TenantID == ""
|
|
}
|
|
|
|
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
|
|
}
|