2018-12-10 22:23:30 +01:00
package authentication
import (
"bytes"
2019-11-26 01:03:57 +01:00
"context"
2018-12-10 22:23:30 +01:00
"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 azureCliTokenAuth struct {
2019-11-26 01:03:57 +01:00
profile * azureCLIProfile
servicePrincipalAuthDocsLink string
2018-12-10 22:23:30 +01:00
}
func ( a azureCliTokenAuth ) build ( b Builder ) ( authMethod , error ) {
auth := azureCliTokenAuth {
profile : & azureCLIProfile {
clientId : b . ClientID ,
environment : b . Environment ,
subscriptionId : b . SubscriptionID ,
tenantId : b . TenantID ,
} ,
2019-11-26 01:03:57 +01:00
servicePrincipalAuthDocsLink : b . ClientSecretDocsLink ,
2018-12-10 22:23:30 +01:00
}
profilePath , err := cli . ProfilePath ( )
if err != nil {
return nil , fmt . Errorf ( "Error loading the Profile Path from the Azure CLI: %+v" , err )
}
profile , err := cli . LoadProfile ( profilePath )
if err != nil {
return nil , fmt . Errorf ( "Azure CLI Authorization Profile was not found. Please ensure the Azure CLI is installed and then log-in with `az login`." )
}
auth . profile . profile = profile
2019-11-26 01:03:57 +01:00
// 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 authenticatedAsAUser := auth . profile . verifyAuthenticatedAsAUser ( ) ; ! authenticatedAsAUser {
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 )
}
2018-12-10 22:23:30 +01:00
err = auth . profile . populateFields ( )
if err != nil {
2019-02-01 09:40:50 +01:00
return nil , fmt . Errorf ( "Error retrieving the Profile from the Azure CLI: %s Please re-authenticate using `az login`." , err )
2018-12-10 22:23:30 +01:00
}
err = auth . profile . populateClientId ( )
if err != nil {
return nil , fmt . Errorf ( "Error populating Client ID from the Azure CLI: %+v" , err )
}
return auth , nil
}
func ( a azureCliTokenAuth ) isApplicable ( b Builder ) bool {
return b . SupportsAzureCliToken
}
2019-11-26 01:03:57 +01:00
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" )
}
2018-12-10 22:23:30 +01:00
// 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 )
}
2019-11-26 01:03:57 +01:00
spt , err := adal . NewServicePrincipalTokenFromManualToken ( * oauth . OAuth , a . profile . clientId , endpoint , adalToken )
2018-12-10 22:23:30 +01:00
if err != nil {
return nil , err
}
2019-11-26 01:03:57 +01:00
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 )
2018-12-10 22:23:30 +01:00
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
2019-11-26 01:03:57 +01:00
c . TenantID = a . profile . tenantId
2018-12-10 22:23:30 +01:00
c . Environment = a . profile . environment
c . SubscriptionID = a . profile . subscriptionId
2019-11-26 01:03:57 +01:00
c . GetAuthenticatedObjectID = func ( ctx context . Context ) ( string , error ) {
objectId , err := obtainAuthenticatedObjectID ( )
if err != nil {
return "" , err
}
return objectId , nil
}
2018-12-10 22:23:30 +01:00
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 ( )
}
2019-11-26 01:03:57 +01:00
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
}
2018-12-10 22:23:30 +01:00
func obtainAuthorizationToken ( endpoint string , subscriptionId string ) ( * cli . Token , error ) {
2019-11-26 01:03:57 +01:00
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
}
func jsonUnmarshalAzCmd ( i interface { } , arg ... string ) error {
2018-12-10 22:23:30 +01:00
var stderr bytes . Buffer
var stdout bytes . Buffer
2019-11-26 01:03:57 +01:00
cmd := exec . Command ( "az" , arg ... )
2018-12-10 22:23:30 +01:00
cmd . Stderr = & stderr
cmd . Stdout = & stdout
if err := cmd . Start ( ) ; err != nil {
2019-11-26 01:03:57 +01:00
return fmt . Errorf ( "Error launching Azure CLI: %+v" , err )
2018-12-10 22:23:30 +01:00
}
if err := cmd . Wait ( ) ; err != nil {
2019-11-26 01:03:57 +01:00
return fmt . Errorf ( "Error waiting for the Azure CLI: %+v" , err )
2018-12-10 22:23:30 +01:00
}
stdOutStr := stdout . String ( )
stdErrStr := stderr . String ( )
if stdErrStr != "" {
2019-11-26 01:03:57 +01:00
return fmt . Errorf ( "Error retrieving running Azure CLI: %s" , strings . TrimSpace ( stdErrStr ) )
2018-12-10 22:23:30 +01:00
}
2019-11-26 01:03:57 +01:00
if err := json . Unmarshal ( [ ] byte ( stdOutStr ) , & i ) ; err != nil {
return fmt . Errorf ( "Error unmarshaling the result of Azure CLI: %v" , err )
2018-12-10 22:23:30 +01:00
}
2019-11-26 01:03:57 +01:00
return nil
2018-12-10 22:23:30 +01:00
}