231 lines
6.7 KiB
Go
231 lines
6.7 KiB
Go
package authentication
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/Azure/go-autorest/autorest"
|
|
"github.com/Azure/go-autorest/autorest/adal"
|
|
"github.com/Azure/go-autorest/autorest/azure/cli"
|
|
"github.com/hashicorp/go-multierror"
|
|
)
|
|
|
|
type azureCLIProfile struct {
|
|
subscription *cli.Subscription
|
|
|
|
clientId string
|
|
environment string
|
|
subscriptionId string
|
|
tenantId string
|
|
}
|
|
|
|
type azureCliTokenAuth struct {
|
|
profile *azureCLIProfile
|
|
servicePrincipalAuthDocsLink string
|
|
}
|
|
|
|
func (a azureCliTokenAuth) build(b Builder) (authMethod, error) {
|
|
auth := azureCliTokenAuth{
|
|
profile: &azureCLIProfile{
|
|
subscriptionId: b.SubscriptionID,
|
|
tenantId: b.TenantID,
|
|
clientId: "04b07795-8ddb-461a-bbee-02f9e1bf7b46", // fixed first party client id for Az CLI
|
|
},
|
|
servicePrincipalAuthDocsLink: b.ClientSecretDocsLink,
|
|
}
|
|
|
|
sub, err := obtainSubscription(b.SubscriptionID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("obtain subscription(%s) from Azure CLI: %+v", b.SubscriptionID, err)
|
|
}
|
|
auth.profile.subscription = sub
|
|
|
|
// Authenticating as a Service Principal doesn't return all of the information we need for authentication purposes
|
|
// as such Service Principal authentication is supported using the specific auth method
|
|
if sub.User == nil || !strings.EqualFold(sub.User.Type, "user") {
|
|
return nil, fmt.Errorf(`Authenticating using the Azure CLI is only supported as a User (not a Service Principal).
|
|
|
|
To authenticate to Azure using a Service Principal, you can use the separate 'Authenticate using a Service Principal'
|
|
auth method - instructions for which can be found here: %s
|
|
|
|
Alternatively you can authenticate using the Azure CLI by using a User Account.`, auth.servicePrincipalAuthDocsLink)
|
|
}
|
|
|
|
// Populate fields
|
|
if auth.profile.subscriptionId == "" {
|
|
auth.profile.subscriptionId = sub.ID
|
|
}
|
|
if auth.profile.tenantId == "" {
|
|
auth.profile.tenantId = sub.TenantID
|
|
}
|
|
// always pull the environment from the Azure CLI, since the Access Token's associated with it
|
|
auth.profile.environment = normalizeEnvironmentName(sub.EnvironmentName)
|
|
|
|
return auth, nil
|
|
}
|
|
|
|
func (a azureCliTokenAuth) isApplicable(b Builder) bool {
|
|
return b.SupportsAzureCliToken
|
|
}
|
|
|
|
func (a azureCliTokenAuth) getAuthorizationToken(sender autorest.Sender, oauth *OAuthConfig, endpoint string) (autorest.Authorizer, error) {
|
|
if oauth.OAuth == nil {
|
|
return nil, fmt.Errorf("Error getting Authorization Token for cli auth: an OAuth token wasn't configured correctly; please file a bug with more details")
|
|
}
|
|
|
|
// the Azure CLI appears to cache these, so to maintain compatibility with the interface this method is intentionally not on the pointer
|
|
token, err := obtainAuthorizationToken(endpoint, a.profile.subscriptionId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error obtaining Authorization Token from the Azure CLI: %s", err)
|
|
}
|
|
|
|
adalToken, err := token.ToADALToken()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error converting Authorization Token to an ADAL Token: %s", err)
|
|
}
|
|
|
|
spt, err := adal.NewServicePrincipalTokenFromManualToken(*oauth.OAuth, a.profile.clientId, endpoint, adalToken)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var refreshFunc adal.TokenRefresh = func(ctx context.Context, resource string) (*adal.Token, error) {
|
|
token, err := obtainAuthorizationToken(resource, a.profile.subscriptionId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
adalToken, err := token.ToADALToken()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &adalToken, nil
|
|
}
|
|
spt.SetCustomRefreshFunc(refreshFunc)
|
|
|
|
auth := autorest.NewBearerAuthorizer(spt)
|
|
return auth, nil
|
|
}
|
|
|
|
func (a azureCliTokenAuth) name() string {
|
|
return "Obtaining a token from the Azure CLI"
|
|
}
|
|
|
|
func (a azureCliTokenAuth) populateConfig(c *Config) error {
|
|
c.ClientID = a.profile.clientId
|
|
c.TenantID = a.profile.tenantId
|
|
c.Environment = a.profile.environment
|
|
c.SubscriptionID = a.profile.subscriptionId
|
|
|
|
c.GetAuthenticatedObjectID = func(ctx context.Context) (string, error) {
|
|
objectId, err := obtainAuthenticatedObjectID()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return objectId, nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a azureCliTokenAuth) validate() error {
|
|
var err *multierror.Error
|
|
|
|
errorMessageFmt := "A %s was not found in your Azure CLI Credentials.\n\nPlease login to the Azure CLI again via `az login`"
|
|
|
|
if a.profile == nil {
|
|
return fmt.Errorf("Azure CLI Profile is nil - this is an internal error and should be reported.")
|
|
}
|
|
|
|
if a.profile.clientId == "" {
|
|
err = multierror.Append(err, fmt.Errorf(errorMessageFmt, "Client ID"))
|
|
}
|
|
|
|
if a.profile.subscriptionId == "" {
|
|
err = multierror.Append(err, fmt.Errorf(errorMessageFmt, "Subscription ID"))
|
|
}
|
|
|
|
if a.profile.tenantId == "" {
|
|
err = multierror.Append(err, fmt.Errorf(errorMessageFmt, "Tenant ID"))
|
|
}
|
|
|
|
return err.ErrorOrNil()
|
|
}
|
|
|
|
func obtainAuthenticatedObjectID() (string, error) {
|
|
|
|
var json struct {
|
|
ObjectId string `json:"objectId"`
|
|
}
|
|
|
|
err := jsonUnmarshalAzCmd(&json, "ad", "signed-in-user", "show", "-o=json")
|
|
if err != nil {
|
|
return "", fmt.Errorf("Error parsing json result from the Azure CLI: %v", err)
|
|
}
|
|
|
|
return json.ObjectId, nil
|
|
}
|
|
|
|
func obtainAuthorizationToken(endpoint string, subscriptionId string) (*cli.Token, error) {
|
|
var token cli.Token
|
|
err := jsonUnmarshalAzCmd(&token, "account", "get-access-token", "--resource", endpoint, "--subscription", subscriptionId, "-o=json")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error parsing json result from the Azure CLI: %v", err)
|
|
}
|
|
|
|
return &token, nil
|
|
}
|
|
|
|
// obtainSubscription return a subscription object of the specified subscriptionId.
|
|
// If the subscriptionId is empty, it returns the default subscription.
|
|
func obtainSubscription(subscriptionId string) (*cli.Subscription, error) {
|
|
var sub cli.Subscription
|
|
cmd := make([]string, 0)
|
|
cmd = []string{"account", "show", "-o=json"}
|
|
if subscriptionId != "" {
|
|
cmd = append(cmd, "-s", subscriptionId)
|
|
}
|
|
err := jsonUnmarshalAzCmd(&sub, cmd...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error parsing json result from the Azure CLI: %v", err)
|
|
}
|
|
|
|
return &sub, nil
|
|
}
|
|
|
|
func jsonUnmarshalAzCmd(i interface{}, arg ...string) error {
|
|
var stderr bytes.Buffer
|
|
var stdout bytes.Buffer
|
|
|
|
cmd := exec.Command("az", arg...)
|
|
|
|
cmd.Stderr = &stderr
|
|
cmd.Stdout = &stdout
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("Error launching Azure CLI: %+v", err)
|
|
}
|
|
|
|
if err := cmd.Wait(); err != nil {
|
|
return fmt.Errorf("Error waiting for the Azure CLI: %+v", err)
|
|
}
|
|
|
|
stdOutStr := stdout.String()
|
|
stdErrStr := stderr.String()
|
|
if stdErrStr != "" {
|
|
return fmt.Errorf("Error retrieving running Azure CLI: %s", strings.TrimSpace(stdErrStr))
|
|
}
|
|
|
|
if err := json.Unmarshal([]byte(stdOutStr), &i); err != nil {
|
|
return fmt.Errorf("Error unmarshaling the result of Azure CLI: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|