diff --git a/autorest/adal/cli.go b/autorest/adal/cli.go deleted file mode 100644 index 524ab38b3..000000000 --- a/autorest/adal/cli.go +++ /dev/null @@ -1,59 +0,0 @@ -package adal - -import ( - "github.com/mitchellh/go-homedir" - "strconv" -) - -// AzureCLIToken represents an AccessToken from the Azure CLI -type AzureCLIToken struct { - AccessToken string `json:"accessToken"` - Authority string `json:"_authority"` - ClientID string `json:"_clientId"` - ExpiresIn int `json:"expiresIn"` - ExpiresOn string `json:"expiresOn"` - IdentityProvider string `json:"identityProvider"` - IsMRRT bool `json:"isMRRT"` - RefreshToken string `json:"refreshToken"` - Resource string `json:"resource"` - TokenType string `json:"tokenType"` - UserID string `json:"userId"` -} - -// AzureCLIProfile represents a Profile from the Azure CLI -type AzureCLIProfile struct { - InstallationID string `json:"installationId"` - Subscriptions []AzureCLISubscription `json:"subscriptions"` -} - -// AzureCLISubscription represents a Subscription from the Azure CLI -type AzureCLISubscription struct { - EnvironmentName string `json:"environmentName"` - ID string `json:"id"` - IsDefault bool `json:"isDefault"` - Name string `json:"name"` - State string `json:"state"` - TenantID string `json:"tenantId"` -} - -// AzureCLIAccessTokensPath returns the path where access tokens are stored from the Azure CLI -func AzureCLIAccessTokensPath() (string, error) { - return homedir.Expand("~/.azure/accessTokens.json") -} - -// AzureCLIProfilePath returns the path where the Azure Profile is stored from the Azure CLI -func AzureCLIProfilePath() (string, error) { - return homedir.Expand("~/.azure/azureProfile.json") -} - -// ToToken converts an AzureCLIToken to a Token -func (t AzureCLIToken) ToToken() Token { - return Token{ - AccessToken: t.AccessToken, - Type: t.TokenType, - ExpiresIn: strconv.Itoa(t.ExpiresIn), - ExpiresOn: t.ExpiresOn, - RefreshToken: t.RefreshToken, - Resource: t.Resource, - } -} diff --git a/autorest/adal/persist.go b/autorest/adal/persist.go index 2ce2667dd..73711c667 100644 --- a/autorest/adal/persist.go +++ b/autorest/adal/persist.go @@ -1,14 +1,11 @@ package adal import ( - "bytes" "encoding/json" "fmt" "io/ioutil" "os" "path/filepath" - - "github.com/dimchansky/utfbom" ) // LoadToken restores a Token object from a file located at 'path'. @@ -28,42 +25,6 @@ func LoadToken(path string) (*Token, error) { return &token, nil } -// LoadCLITokens restores a set of AzureCLIToken objects from a file located at 'path'. -func LoadCLITokens(path string) ([]AzureCLIToken, error) { - file, err := os.Open(path) - if err != nil { - return nil, fmt.Errorf("failed to open file (%s) while loading token: %v", path, err) - } - defer file.Close() - - var tokens []AzureCLIToken - - dec := json.NewDecoder(file) - if err = dec.Decode(&tokens); err != nil { - return nil, fmt.Errorf("failed to decode contents of file (%s) into a AzureCLIToken representation: %v", path, err) - } - - return tokens, nil -} - -// LoadCLIProfile restores an AzureCLIProfile object from a file located at 'path'. -func LoadCLIProfile(path string) (AzureCLIProfile, error) { - var profile AzureCLIProfile - - contents, err := ioutil.ReadFile(path) - if err != nil { - return profile, fmt.Errorf("failed to open file (%s) while loading token: %v", path, err) - } - reader := utfbom.SkipOnly(bytes.NewReader(contents)) - - dec := json.NewDecoder(reader) - if err = dec.Decode(&profile); err != nil { - return profile, fmt.Errorf("failed to decode contents of file (%s) into a AzureCLIProfile representation: %v", path, err) - } - - return profile, nil -} - // SaveToken persists an oauth token at the given location on disk. // It moves the new file into place so it can safely be used to replace an existing file // that maybe accessed by multiple processes. diff --git a/autorest/adal/token.go b/autorest/adal/token.go index 559fc6653..4152426d6 100644 --- a/autorest/adal/token.go +++ b/autorest/adal/token.go @@ -15,12 +15,12 @@ import ( "strings" "time" + "github.com/Azure/go-autorest/autorest/date" "github.com/dgrijalva/jwt-go" ) const ( defaultRefresh = 5 * time.Minute - tokenBaseDate = "1970-01-01T00:00:00Z" // OAuthGrantTypeDeviceCode is the "grant_type" identifier used in device flow OAuthGrantTypeDeviceCode = "device_code" @@ -35,12 +35,6 @@ const ( managedIdentitySettingsPath = "/var/lib/waagent/ManagedIdentity-Settings" ) -var expirationBase time.Time - -func init() { - expirationBase, _ = time.Parse(time.RFC3339, tokenBaseDate) -} - // OAuthTokenProvider is an interface which should be implemented by an access token retriever type OAuthTokenProvider interface { OAuthToken() string @@ -76,7 +70,10 @@ func (t Token) Expires() time.Time { if err != nil { s = -3600 } - return expirationBase.Add(time.Duration(s) * time.Second).UTC() + + expiration := date.NewUnixTimeFromSeconds(float64(s)) + + return time.Time(expiration).UTC() } // IsExpired returns true if the Token is expired, false otherwise. diff --git a/autorest/adal/token_test.go b/autorest/adal/token_test.go index 9c92f4198..bec7a79a6 100644 --- a/autorest/adal/token_test.go +++ b/autorest/adal/token_test.go @@ -17,6 +17,7 @@ import ( "testing" "time" + "github.com/Azure/go-autorest/autorest/date" "github.com/Azure/go-autorest/autorest/mocks" ) @@ -342,7 +343,7 @@ func TestServicePrincipalTokenRefreshReturnsErrorIfNotOk(t *testing.T) { func TestServicePrincipalTokenRefreshUnmarshals(t *testing.T) { spt := newServicePrincipalToken() - expiresOn := strconv.Itoa(int(time.Now().Add(3600 * time.Second).Sub(expirationBase).Seconds())) + expiresOn := strconv.Itoa(int(time.Now().Add(3600 * time.Second).Sub(date.UnixEpoch()).Seconds())) j := newTokenJSON(expiresOn, "resource") resp := mocks.NewResponseWithContent(j) c := mocks.NewSender() @@ -430,7 +431,7 @@ func TestRefreshCallback(t *testing.T) { return nil }) - expiresOn := strconv.Itoa(int(time.Now().Add(3600 * time.Second).Sub(expirationBase).Seconds())) + expiresOn := strconv.Itoa(int(time.Now().Add(3600 * time.Second).Sub(date.UnixEpoch()).Seconds())) sender := mocks.NewSender() j := newTokenJSON(expiresOn, "resource") @@ -451,7 +452,7 @@ func TestRefreshCallbackErrorPropagates(t *testing.T) { return fmt.Errorf(errorText) }) - expiresOn := strconv.Itoa(int(time.Now().Add(3600 * time.Second).Sub(expirationBase).Seconds())) + expiresOn := strconv.Itoa(int(time.Now().Add(3600 * time.Second).Sub(date.UnixEpoch()).Seconds())) sender := mocks.NewSender() j := newTokenJSON(expiresOn, "resource") @@ -554,7 +555,7 @@ func expireToken(t *Token) *Token { func setTokenToExpireAt(t *Token, expireAt time.Time) *Token { t.ExpiresIn = "3600" - t.ExpiresOn = strconv.Itoa(int(expireAt.Sub(expirationBase).Seconds())) + t.ExpiresOn = strconv.Itoa(int(expireAt.Sub(date.UnixEpoch()).Seconds())) t.NotBefore = t.ExpiresOn return t } diff --git a/autorest/azure/cli/profile.go b/autorest/azure/cli/profile.go new file mode 100644 index 000000000..b5b897c7d --- /dev/null +++ b/autorest/azure/cli/profile.go @@ -0,0 +1,51 @@ +package cli + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + + "github.com/dimchansky/utfbom" + "github.com/mitchellh/go-homedir" +) + +// Profile represents a Profile from the Azure CLI +type Profile struct { + InstallationID string `json:"installationId"` + Subscriptions []Subscription `json:"subscriptions"` +} + +// Subscription represents a Subscription from the Azure CLI +type Subscription struct { + EnvironmentName string `json:"environmentName"` + ID string `json:"id"` + IsDefault bool `json:"isDefault"` + Name string `json:"name"` + State string `json:"state"` + TenantID string `json:"tenantId"` +} + +// ProfilePath returns the path where the Azure Profile is stored from the Azure CLI +func ProfilePath() (string, error) { + return homedir.Expand("~/.azure/azureProfile.json") +} + +// LoadProfile restores a Profile object from a file located at 'path'. +func LoadProfile(path string) (result Profile, err error) { + var contents []byte + contents, err = ioutil.ReadFile(path) + if err != nil { + err = fmt.Errorf("failed to open file (%s) while loading token: %v", path, err) + return + } + reader := utfbom.SkipOnly(bytes.NewReader(contents)) + + dec := json.NewDecoder(reader) + if err = dec.Decode(&result); err != nil { + err = fmt.Errorf("failed to decode contents of file (%s) into a Profile representation: %v", path, err) + return + } + + return +} diff --git a/autorest/azure/cli/token.go b/autorest/azure/cli/token.go new file mode 100644 index 000000000..a1f3af151 --- /dev/null +++ b/autorest/azure/cli/token.go @@ -0,0 +1,89 @@ +package cli + +import ( + "encoding/json" + "fmt" + "os" + "strconv" + "time" + + "github.com/Azure/go-autorest/autorest/adal" + "github.com/Azure/go-autorest/autorest/date" + "github.com/mitchellh/go-homedir" +) + +// Token represents an AccessToken from the Azure CLI +type Token struct { + AccessToken string `json:"accessToken"` + Authority string `json:"_authority"` + ClientID string `json:"_clientId"` + ExpiresOn string `json:"expiresOn"` + IdentityProvider string `json:"identityProvider"` + IsMRRT bool `json:"isMRRT"` + RefreshToken string `json:"refreshToken"` + Resource string `json:"resource"` + TokenType string `json:"tokenType"` + UserID string `json:"userId"` +} + +// ToADALToken converts an Azure CLI `Token`` to an `adal.Token`` +func (t Token) ToADALToken() (converted adal.Token, err error) { + tokenExpirationDate, err := ParseExpirationDate(t.ExpiresOn) + if err != nil { + err = fmt.Errorf("Error parsing Token Expiration Date %q: %+v", t.ExpiresOn, err) + return + } + + difference := tokenExpirationDate.Sub(date.UnixEpoch()) + + converted = adal.Token{ + AccessToken: t.AccessToken, + Type: t.TokenType, + ExpiresIn: "3600", + ExpiresOn: strconv.Itoa(int(difference.Seconds())), + RefreshToken: t.RefreshToken, + Resource: t.Resource, + } + return +} + +// AccessTokensPath returns the path where access tokens are stored from the Azure CLI +func AccessTokensPath() (string, error) { + return homedir.Expand("~/.azure/accessTokens.json") +} + +// ParseExpirationDate parses either a Azure CLI or CloudShell date into a time object +func ParseExpirationDate(input string) (*time.Time, error) { + // CloudShell (and potentially the Azure CLI in future) + expirationDate, cloudShellErr := time.Parse(time.RFC3339, input) + if cloudShellErr != nil { + // Azure CLI (Python) e.g. 2017-08-31 19:48:57.998857 (plus the local timezone) + const cliFormat = "2006-01-02 15:04:05.999999" + expirationDate, cliErr := time.ParseInLocation(cliFormat, input, time.Local) + if cliErr == nil { + return &expirationDate, nil + } + + return nil, fmt.Errorf("Error parsing expiration date %q.\n\nCloudShell Error: \n%+v\n\nCLI Error:\n%+v", input, cloudShellErr, cliErr) + } + + return &expirationDate, nil +} + +// LoadTokens restores a set of Token objects from a file located at 'path'. +func LoadTokens(path string) ([]Token, error) { + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to open file (%s) while loading token: %v", path, err) + } + defer file.Close() + + var tokens []Token + + dec := json.NewDecoder(file) + if err = dec.Decode(&tokens); err != nil { + return nil, fmt.Errorf("failed to decode contents of file (%s) into a `cli.Token` representation: %v", path, err) + } + + return tokens, nil +}