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"
)
2020-08-07 10:45:40 +02:00
type azureCLIProfile struct {
subscription * cli . Subscription
clientId string
environment string
subscriptionId string
tenantId string
}
2018-12-10 22:23:30 +01:00
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 {
subscriptionId : b . SubscriptionID ,
tenantId : b . TenantID ,
2020-08-07 10:45:40 +02:00
clientId : "04b07795-8ddb-461a-bbee-02f9e1bf7b46" , // fixed first party client id for Az CLI
2018-12-10 22:23:30 +01:00
} ,
2019-11-26 01:03:57 +01:00
servicePrincipalAuthDocsLink : b . ClientSecretDocsLink ,
2018-12-10 22:23:30 +01:00
}
2020-08-07 10:45:40 +02:00
sub , err := obtainSubscription ( b . SubscriptionID )
2018-12-10 22:23:30 +01:00
if err != nil {
2020-08-07 10:45:40 +02:00
return nil , fmt . Errorf ( "obtain subscription(%s) from Azure CLI: %+v" , b . SubscriptionID , err )
2018-12-10 22:23:30 +01:00
}
2020-08-07 10:45:40 +02:00
auth . profile . subscription = sub
2018-12-10 22:23:30 +01:00
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
2020-08-07 10:45:40 +02:00
if sub . User == nil || ! strings . EqualFold ( sub . User . Type , "user" ) {
2019-11-26 01:03:57 +01:00
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 )
}
2020-08-07 10:45:40 +02:00
// Populate fields
if auth . profile . subscriptionId == "" {
auth . profile . subscriptionId = sub . ID
2018-12-10 22:23:30 +01:00
}
2020-08-07 10:45:40 +02:00
if auth . profile . tenantId == "" {
auth . profile . tenantId = sub . TenantID
2018-12-10 22:23:30 +01:00
}
2020-08-07 10:45:40 +02:00
// always pull the environment from the Azure CLI, since the Access Token's associated with it
auth . profile . environment = normalizeEnvironmentName ( sub . EnvironmentName )
2018-12-10 22:23:30 +01:00
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
}
2020-08-07 10:45:40 +02:00
// 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
}
2019-11-26 01:03:57 +01:00
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
}