...

Source file src/github.com/Azure/go-autorest/autorest/azure/cli/token.go

Documentation: github.com/Azure/go-autorest/autorest/azure/cli

     1  package cli
     2  
     3  // Copyright 2017 Microsoft Corporation
     4  //
     5  //  Licensed under the Apache License, Version 2.0 (the "License");
     6  //  you may not use this file except in compliance with the License.
     7  //  You may obtain a copy of the License at
     8  //
     9  //      http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  //  Unless required by applicable law or agreed to in writing, software
    12  //  distributed under the License is distributed on an "AS IS" BASIS,
    13  //  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  //  See the License for the specific language governing permissions and
    15  //  limitations under the License.
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/json"
    20  	"fmt"
    21  	"os"
    22  	"os/exec"
    23  	"path/filepath"
    24  	"regexp"
    25  	"runtime"
    26  	"strconv"
    27  	"time"
    28  
    29  	"github.com/Azure/go-autorest/autorest/adal"
    30  	"github.com/Azure/go-autorest/autorest/date"
    31  	"github.com/mitchellh/go-homedir"
    32  )
    33  
    34  // Token represents an AccessToken from the Azure CLI
    35  type Token struct {
    36  	AccessToken      string `json:"accessToken"`
    37  	Authority        string `json:"_authority"`
    38  	ClientID         string `json:"_clientId"`
    39  	ExpiresOn        string `json:"expiresOn"`
    40  	IdentityProvider string `json:"identityProvider"`
    41  	IsMRRT           bool   `json:"isMRRT"`
    42  	RefreshToken     string `json:"refreshToken"`
    43  	Resource         string `json:"resource"`
    44  	TokenType        string `json:"tokenType"`
    45  	UserID           string `json:"userId"`
    46  }
    47  
    48  const accessTokensJSON = "accessTokens.json"
    49  
    50  // ToADALToken converts an Azure CLI `Token`` to an `adal.Token``
    51  func (t Token) ToADALToken() (converted adal.Token, err error) {
    52  	tokenExpirationDate, err := ParseExpirationDate(t.ExpiresOn)
    53  	if err != nil {
    54  		err = fmt.Errorf("Error parsing Token Expiration Date %q: %+v", t.ExpiresOn, err)
    55  		return
    56  	}
    57  
    58  	difference := tokenExpirationDate.Sub(date.UnixEpoch())
    59  
    60  	converted = adal.Token{
    61  		AccessToken:  t.AccessToken,
    62  		Type:         t.TokenType,
    63  		ExpiresIn:    "3600",
    64  		ExpiresOn:    json.Number(strconv.Itoa(int(difference.Seconds()))),
    65  		RefreshToken: t.RefreshToken,
    66  		Resource:     t.Resource,
    67  	}
    68  	return
    69  }
    70  
    71  // AccessTokensPath returns the path where access tokens are stored from the Azure CLI
    72  // TODO(#199): add unit test.
    73  func AccessTokensPath() (string, error) {
    74  	// Azure-CLI allows user to customize the path of access tokens through environment variable.
    75  	if accessTokenPath := os.Getenv("AZURE_ACCESS_TOKEN_FILE"); accessTokenPath != "" {
    76  		return accessTokenPath, nil
    77  	}
    78  
    79  	// Azure-CLI allows user to customize the path to Azure config directory through environment variable.
    80  	if cfgDir := configDir(); cfgDir != "" {
    81  		return filepath.Join(cfgDir, accessTokensJSON), nil
    82  	}
    83  
    84  	// Fallback logic to default path on non-cloud-shell environment.
    85  	// TODO(#200): remove the dependency on hard-coding path.
    86  	return homedir.Expand("~/.azure/" + accessTokensJSON)
    87  }
    88  
    89  // ParseExpirationDate parses either a Azure CLI or CloudShell date into a time object
    90  func ParseExpirationDate(input string) (*time.Time, error) {
    91  	// CloudShell (and potentially the Azure CLI in future)
    92  	expirationDate, cloudShellErr := time.Parse(time.RFC3339, input)
    93  	if cloudShellErr != nil {
    94  		// Azure CLI (Python) e.g. 2017-08-31 19:48:57.998857 (plus the local timezone)
    95  		const cliFormat = "2006-01-02 15:04:05.999999"
    96  		expirationDate, cliErr := time.ParseInLocation(cliFormat, input, time.Local)
    97  		if cliErr == nil {
    98  			return &expirationDate, nil
    99  		}
   100  
   101  		return nil, fmt.Errorf("Error parsing expiration date %q.\n\nCloudShell Error: \n%+v\n\nCLI Error:\n%+v", input, cloudShellErr, cliErr)
   102  	}
   103  
   104  	return &expirationDate, nil
   105  }
   106  
   107  // LoadTokens restores a set of Token objects from a file located at 'path'.
   108  func LoadTokens(path string) ([]Token, error) {
   109  	file, err := os.Open(path)
   110  	if err != nil {
   111  		return nil, fmt.Errorf("failed to open file (%s) while loading token: %v", path, err)
   112  	}
   113  	defer file.Close()
   114  
   115  	var tokens []Token
   116  
   117  	dec := json.NewDecoder(file)
   118  	if err = dec.Decode(&tokens); err != nil {
   119  		return nil, fmt.Errorf("failed to decode contents of file (%s) into a `cli.Token` representation: %v", path, err)
   120  	}
   121  
   122  	return tokens, nil
   123  }
   124  
   125  // GetTokenFromCLI gets a token using Azure CLI 2.0 for local development scenarios.
   126  func GetTokenFromCLI(resource string) (*Token, error) {
   127  	return GetTokenFromCLIWithParams(GetAccessTokenParams{Resource: resource})
   128  }
   129  
   130  // GetAccessTokenParams is the parameter struct of GetTokenFromCLIWithParams
   131  type GetAccessTokenParams struct {
   132  	Resource     string
   133  	ResourceType string
   134  	Subscription string
   135  	Tenant       string
   136  }
   137  
   138  // GetTokenFromCLIWithParams gets a token using Azure CLI 2.0 for local development scenarios.
   139  func GetTokenFromCLIWithParams(params GetAccessTokenParams) (*Token, error) {
   140  	cliCmd := GetAzureCLICommand()
   141  
   142  	cliCmd.Args = append(cliCmd.Args, "account", "get-access-token", "-o", "json")
   143  	if params.Resource != "" {
   144  		if err := validateParameter(params.Resource); err != nil {
   145  			return nil, err
   146  		}
   147  		cliCmd.Args = append(cliCmd.Args, "--resource", params.Resource)
   148  	}
   149  	if params.ResourceType != "" {
   150  		if err := validateParameter(params.ResourceType); err != nil {
   151  			return nil, err
   152  		}
   153  		cliCmd.Args = append(cliCmd.Args, "--resource-type", params.ResourceType)
   154  	}
   155  	if params.Subscription != "" {
   156  		if err := validateParameter(params.Subscription); err != nil {
   157  			return nil, err
   158  		}
   159  		cliCmd.Args = append(cliCmd.Args, "--subscription", params.Subscription)
   160  	}
   161  	if params.Tenant != "" {
   162  		if err := validateParameter(params.Tenant); err != nil {
   163  			return nil, err
   164  		}
   165  		cliCmd.Args = append(cliCmd.Args, "--tenant", params.Tenant)
   166  	}
   167  
   168  	var stderr bytes.Buffer
   169  	cliCmd.Stderr = &stderr
   170  
   171  	output, err := cliCmd.Output()
   172  	if err != nil {
   173  		if stderr.Len() > 0 {
   174  			return nil, fmt.Errorf("Invoking Azure CLI failed with the following error: %s", stderr.String())
   175  		}
   176  
   177  		return nil, fmt.Errorf("Invoking Azure CLI failed with the following error: %s", err.Error())
   178  	}
   179  
   180  	tokenResponse := Token{}
   181  	err = json.Unmarshal(output, &tokenResponse)
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	return &tokenResponse, err
   187  }
   188  
   189  func validateParameter(param string) error {
   190  	// Validate parameters, since it gets sent as a command line argument to Azure CLI
   191  	const invalidResourceErrorTemplate = "Parameter %s is not in expected format. Only alphanumeric characters, [dot], [colon], [hyphen], and [forward slash] are allowed."
   192  	match, err := regexp.MatchString("^[0-9a-zA-Z-.:/]+$", param)
   193  	if err != nil {
   194  		return err
   195  	}
   196  	if !match {
   197  		return fmt.Errorf(invalidResourceErrorTemplate, param)
   198  	}
   199  	return nil
   200  }
   201  
   202  // GetAzureCLICommand can be used to run arbitrary Azure CLI command
   203  func GetAzureCLICommand() *exec.Cmd {
   204  	// This is the path that a developer can set to tell this class what the install path for Azure CLI is.
   205  	const azureCLIPath = "AzureCLIPath"
   206  
   207  	// The default install paths are used to find Azure CLI. This is for security, so that any path in the calling program's Path environment is not used to execute Azure CLI.
   208  	azureCLIDefaultPathWindows := fmt.Sprintf("%s\\Microsoft SDKs\\Azure\\CLI2\\wbin; %s\\Microsoft SDKs\\Azure\\CLI2\\wbin", os.Getenv("ProgramFiles(x86)"), os.Getenv("ProgramFiles"))
   209  
   210  	// Default path for non-Windows.
   211  	const azureCLIDefaultPath = "/bin:/sbin:/usr/bin:/usr/local/bin"
   212  
   213  	// Execute Azure CLI to get token
   214  	var cliCmd *exec.Cmd
   215  	if runtime.GOOS == "windows" {
   216  		cliCmd = exec.Command(fmt.Sprintf("%s\\system32\\cmd.exe", os.Getenv("windir")))
   217  		cliCmd.Env = os.Environ()
   218  		cliCmd.Env = append(cliCmd.Env, fmt.Sprintf("PATH=%s;%s", os.Getenv(azureCLIPath), azureCLIDefaultPathWindows))
   219  		cliCmd.Args = append(cliCmd.Args, "/c", "az")
   220  	} else {
   221  		cliCmd = exec.Command("az")
   222  		cliCmd.Env = os.Environ()
   223  		cliCmd.Env = append(cliCmd.Env, fmt.Sprintf("PATH=%s:%s", os.Getenv(azureCLIPath), azureCLIDefaultPath))
   224  	}
   225  
   226  	return cliCmd
   227  }
   228  

View as plain text