diff --git a/.gitignore b/.gitignore index ab262cbe5..3350aaf70 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ _obj _test .DS_Store .idea/ +.vscode/ # Architecture specific extensions/prefixes *.[568vq] diff --git a/CHANGELOG.md b/CHANGELOG.md index 8500cb8df..f42eb81c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # CHANGELOG +## v11.0.0 + +### Breaking Changes + +- To handle differences between ADFS and AAD the following fields have had their types changed from `string` to `json.Number` + - ExpiresIn + - ExpiresOn + - NotBefore + +### New Features + +- Added `auth.NewAuthorizerFromFileWithResource` to create an authorizer from the config file with the specified resource. +- Setting a client's `PollingDuration` to zero will use the provided context to control a LRO's polling duration. + ## v10.15.5 ### Bug Fixes diff --git a/autorest/adal/sender.go b/autorest/adal/sender.go index 0e5ad14d3..834401e00 100644 --- a/autorest/adal/sender.go +++ b/autorest/adal/sender.go @@ -38,7 +38,7 @@ func (sf SenderFunc) Do(r *http.Request) (*http.Response, error) { return sf(r) } -// SendDecorator takes and possibily decorates, by wrapping, a Sender. Decorators may affect the +// SendDecorator takes and possibly decorates, by wrapping, a Sender. Decorators may affect the // http.Request and pass it along or, first, pass the http.Request along then react to the // http.Response result. type SendDecorator func(Sender) Sender diff --git a/autorest/adal/token.go b/autorest/adal/token.go index 32aea8389..6f59bfd08 100644 --- a/autorest/adal/token.go +++ b/autorest/adal/token.go @@ -29,7 +29,6 @@ import ( "net" "net/http" "net/url" - "strconv" "strings" "sync" "time" @@ -97,18 +96,27 @@ type RefresherWithContext interface { type TokenRefreshCallback func(Token) error // Token encapsulates the access token used to authorize Azure requests. +// https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-oauth2-client-creds-grant-flow#service-to-service-access-token-response type Token struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` - ExpiresIn string `json:"expires_in"` - ExpiresOn string `json:"expires_on"` - NotBefore string `json:"not_before"` + ExpiresIn json.Number `json:"expires_in"` + ExpiresOn json.Number `json:"expires_on"` + NotBefore json.Number `json:"not_before"` Resource string `json:"resource"` Type string `json:"token_type"` } +func newToken() Token { + return Token{ + ExpiresIn: "0", + ExpiresOn: "0", + NotBefore: "0", + } +} + // IsZero returns true if the token object is zero-initialized. func (t Token) IsZero() bool { return t == Token{} @@ -116,12 +124,12 @@ func (t Token) IsZero() bool { // Expires returns the time.Time when the Token expires. func (t Token) Expires() time.Time { - s, err := strconv.Atoi(t.ExpiresOn) + s, err := t.ExpiresOn.Float64() if err != nil { s = -3600 } - expiration := date.NewUnixTimeFromSeconds(float64(s)) + expiration := date.NewUnixTimeFromSeconds(s) return time.Time(expiration).UTC() } @@ -414,6 +422,7 @@ func NewServicePrincipalTokenWithSecret(oauthConfig OAuthConfig, id string, reso } spt := &ServicePrincipalToken{ inner: servicePrincipalToken{ + Token: newToken(), OauthConfig: oauthConfig, Secret: secret, ClientID: id, @@ -653,6 +662,7 @@ func newServicePrincipalTokenFromMSI(msiEndpoint, resource string, userAssignedI spt := &ServicePrincipalToken{ inner: servicePrincipalToken{ + Token: newToken(), OauthConfig: OAuthConfig{ TokenEndpoint: *msiEndpointURL, }, diff --git a/autorest/adal/token_test.go b/autorest/adal/token_test.go index 5a2ab8c02..ec8b33ddb 100644 --- a/autorest/adal/token_test.go +++ b/autorest/adal/token_test.go @@ -495,8 +495,8 @@ func TestServicePrincipalTokenRefreshUnmarshals(t *testing.T) { t.Fatalf("adal: ServicePrincipalToken#Refresh returned an unexpected error (%v)", err) } else if spt.inner.Token.AccessToken != "accessToken" || spt.inner.Token.ExpiresIn != "3600" || - spt.inner.Token.ExpiresOn != expiresOn || - spt.inner.Token.NotBefore != expiresOn || + spt.inner.Token.ExpiresOn != json.Number(expiresOn) || + spt.inner.Token.NotBefore != json.Number(expiresOn) || spt.inner.Token.Resource != "resource" || spt.inner.Token.Type != "Bearer" { t.Fatalf("adal: ServicePrincipalToken#Refresh failed correctly unmarshal the JSON -- expected %v, received %v", @@ -684,13 +684,13 @@ func TestNewServicePrincipalTokenFromManualTokenSecret(t *testing.T) { RedirectURI: "redirect", } - spt, err := NewServicePrincipalTokenFromManualTokenSecret(TestOAuthConfig, "id", "resource", *token, secret, nil) + spt, err := NewServicePrincipalTokenFromManualTokenSecret(TestOAuthConfig, "id", "resource", token, secret, nil) if err != nil { t.Fatalf("Failed creating new SPT: %s", err) } - if !reflect.DeepEqual(*token, spt.inner.Token) { - t.Fatalf("Tokens do not match: %s, %s", *token, spt.inner.Token) + if !reflect.DeepEqual(token, spt.inner.Token) { + t.Fatalf("Tokens do not match: %s, %s", token, spt.inner.Token) } if !reflect.DeepEqual(secret, spt.inner.Secret) { @@ -822,7 +822,7 @@ func TestMarshalInnerToken(t *testing.T) { t.Fatalf("tokens don't match: %s, %s", tokenJSON, testTokenJSON) } - var t1 *Token + var t1 Token err = json.Unmarshal(tokenJSON, &t1) if err != nil { t.Fatalf("failed to unmarshal token: %+v", err) @@ -833,14 +833,6 @@ func TestMarshalInnerToken(t *testing.T) { } } -func newToken() *Token { - return &Token{ - AccessToken: "ASECRETVALUE", - Resource: "https://azure.microsoft.com/", - Type: "Bearer", - } -} - func newTokenJSON(expiresOn string, resource string) string { return fmt.Sprintf(`{ "access_token" : "accessToken", @@ -855,11 +847,13 @@ func newTokenJSON(expiresOn string, resource string) string { } func newTokenExpiresIn(expireIn time.Duration) *Token { - return setTokenToExpireIn(newToken(), expireIn) + t := newToken() + return setTokenToExpireIn(&t, expireIn) } func newTokenExpiresAt(expireAt time.Time) *Token { - return setTokenToExpireAt(newToken(), expireAt) + t := newToken() + return setTokenToExpireAt(&t, expireAt) } func expireToken(t *Token) *Token { @@ -868,7 +862,7 @@ func expireToken(t *Token) *Token { func setTokenToExpireAt(t *Token, expireAt time.Time) *Token { t.ExpiresIn = "3600" - t.ExpiresOn = strconv.Itoa(int(expireAt.Sub(date.UnixEpoch()).Seconds())) + t.ExpiresOn = json.Number(strconv.FormatInt(int64(expireAt.Sub(date.UnixEpoch())/time.Second), 10)) t.NotBefore = t.ExpiresOn return t } @@ -885,7 +879,7 @@ func newServicePrincipalToken(callbacks ...TokenRefreshCallback) *ServicePrincip func newServicePrincipalTokenManual() *ServicePrincipalToken { token := newToken() token.RefreshToken = "refreshtoken" - spt, _ := NewServicePrincipalTokenFromManualToken(TestOAuthConfig, "id", "resource", *token) + spt, _ := NewServicePrincipalTokenFromManualToken(TestOAuthConfig, "id", "resource", token) return spt } diff --git a/autorest/azure/async.go b/autorest/azure/async.go index 9dd7a1d27..e43f7a2c4 100644 --- a/autorest/azure/async.go +++ b/autorest/azure/async.go @@ -168,8 +168,12 @@ func (f Future) WaitForCompletion(ctx context.Context, client autorest.Client) e // polling duration has been exceeded. It will retry failed polling attempts based on // the retry value defined in the client up to the maximum retry attempts. func (f *Future) WaitForCompletionRef(ctx context.Context, client autorest.Client) error { - ctx, cancel := context.WithTimeout(ctx, client.PollingDuration) - defer cancel() + if d := client.PollingDuration; d != 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, d) + defer cancel() + } + done, err := f.Done(client) for attempts := 0; !done; done, err = f.Done(client) { if attempts >= client.RetryAttempts { diff --git a/autorest/azure/auth/auth.go b/autorest/azure/auth/auth.go index a14b87900..a52625750 100644 --- a/autorest/azure/auth/auth.go +++ b/autorest/azure/auth/auth.go @@ -135,44 +135,62 @@ func (settings settings) getAuthorizer() (autorest.Authorizer, error) { // NewAuthorizerFromFile creates an Authorizer configured from a configuration file. func NewAuthorizerFromFile(baseURI string) (autorest.Authorizer, error) { - fileLocation := os.Getenv("AZURE_AUTH_LOCATION") - if fileLocation == "" { - return nil, errors.New("auth file not found. Environment variable AZURE_AUTH_LOCATION is not set") + file, err := getAuthFile() + if err != nil { + return nil, err } - contents, err := ioutil.ReadFile(fileLocation) + resource, err := getResourceForToken(*file, baseURI) if err != nil { return nil, err } + return NewAuthorizerFromFileWithResource(resource) +} - // Auth file might be encoded - decoded, err := decode(contents) +// NewAuthorizerFromFileWithResource creates an Authorizer configured from a configuration file. +func NewAuthorizerFromFileWithResource(resource string) (autorest.Authorizer, error) { + file, err := getAuthFile() if err != nil { return nil, err } - file := file{} - err = json.Unmarshal(decoded, &file) + config, err := adal.NewOAuthConfig(file.ActiveDirectoryEndpoint, file.TenantID) if err != nil { return nil, err } - resource, err := getResourceForToken(file, baseURI) + spToken, err := adal.NewServicePrincipalToken(*config, file.ClientID, file.ClientSecret, resource) if err != nil { return nil, err } - config, err := adal.NewOAuthConfig(file.ActiveDirectoryEndpoint, file.TenantID) + return autorest.NewBearerAuthorizer(spToken), nil +} + +func getAuthFile() (*file, error) { + fileLocation := os.Getenv("AZURE_AUTH_LOCATION") + if fileLocation == "" { + return nil, errors.New("environment variable AZURE_AUTH_LOCATION is not set") + } + + contents, err := ioutil.ReadFile(fileLocation) if err != nil { return nil, err } - spToken, err := adal.NewServicePrincipalToken(*config, file.ClientID, file.ClientSecret, resource) + // Auth file might be encoded + decoded, err := decode(contents) if err != nil { return nil, err } - return autorest.NewBearerAuthorizer(spToken), nil + authFile := file{} + err = json.Unmarshal(decoded, &authFile) + if err != nil { + return nil, err + } + + return &authFile, nil } // File represents the authentication file diff --git a/autorest/azure/auth/auth_test.go b/autorest/azure/auth/auth_test.go index 667961bde..44eb61bab 100644 --- a/autorest/azure/auth/auth_test.go +++ b/autorest/azure/auth/auth_test.go @@ -47,6 +47,15 @@ func TestNewAuthorizerFromFile(t *testing.T) { } } +func TestNewAuthorizerFromFileWithResource(t *testing.T) { + os.Setenv("AZURE_AUTH_LOCATION", filepath.Join(getCredsPath(), "credsutf16le.json")) + authorizer, err := NewAuthorizerFromFileWithResource("https://my.vault.azure.net") + if err != nil || authorizer == nil { + t.Logf("NewAuthorizerFromFileWithResource failed, got error %v", err) + t.Fail() + } +} + func TestNewAuthorizerFromEnvironment(t *testing.T) { os.Setenv("AZURE_TENANT_ID", expectedFile.TenantID) os.Setenv("AZURE_CLIENT_ID", expectedFile.ClientID) @@ -54,7 +63,19 @@ func TestNewAuthorizerFromEnvironment(t *testing.T) { authorizer, err := NewAuthorizerFromEnvironment() if err != nil || authorizer == nil { - t.Logf("NewAuthorizerFromFile failed, got error %v", err) + t.Logf("NewAuthorizerFromEnvironment failed, got error %v", err) + t.Fail() + } +} + +func TestNewAuthorizerFromEnvironmentWithResource(t *testing.T) { + os.Setenv("AZURE_TENANT_ID", expectedFile.TenantID) + os.Setenv("AZURE_CLIENT_ID", expectedFile.ClientID) + os.Setenv("AZURE_CLIENT_SECRET", expectedFile.ClientSecret) + authorizer, err := NewAuthorizerFromEnvironmentWithResource("https://my.vault.azure.net") + + if err != nil || authorizer == nil { + t.Logf("NewAuthorizerFromEnvironmentWithResource failed, got error %v", err) t.Fail() } } diff --git a/autorest/azure/cli/token.go b/autorest/azure/cli/token.go index 83b81c34b..ec3704823 100644 --- a/autorest/azure/cli/token.go +++ b/autorest/azure/cli/token.go @@ -54,7 +54,7 @@ func (t Token) ToADALToken() (converted adal.Token, err error) { AccessToken: t.AccessToken, Type: t.TokenType, ExpiresIn: "3600", - ExpiresOn: strconv.Itoa(int(difference.Seconds())), + ExpiresOn: json.Number(strconv.Itoa(int(difference.Seconds()))), RefreshToken: t.RefreshToken, Resource: t.Resource, } diff --git a/autorest/azure/rp.go b/autorest/azure/rp.go index bd34f0ed5..86ce9f2b5 100644 --- a/autorest/azure/rp.go +++ b/autorest/azure/rp.go @@ -140,8 +140,8 @@ func register(client autorest.Client, originalReq *http.Request, re RequestError } // poll for registered provisioning state - now := time.Now() - for err == nil && time.Since(now) < client.PollingDuration { + registrationStartTime := time.Now() + for err == nil && (client.PollingDuration == 0 || (client.PollingDuration != 0 && time.Since(registrationStartTime) < client.PollingDuration)) { // taken from the resources SDK // https://github.com/Azure/azure-sdk-for-go/blob/9f366792afa3e0ddaecdc860e793ba9d75e76c27/arm/resources/resources/providers.go#L45 preparer := autorest.CreatePreparer( @@ -183,7 +183,7 @@ func register(client autorest.Client, originalReq *http.Request, re RequestError return originalReq.Context().Err() } } - if !(time.Since(now) < client.PollingDuration) { + if client.PollingDuration != 0 && !(time.Since(registrationStartTime) < client.PollingDuration) { return errors.New("polling for resource provider registration has exceeded the polling duration") } return err diff --git a/autorest/client.go b/autorest/client.go index 5c558c83a..48eb0e8b7 100644 --- a/autorest/client.go +++ b/autorest/client.go @@ -147,6 +147,7 @@ type Client struct { PollingDelay time.Duration // PollingDuration sets the maximum polling time after which an error is returned. + // Setting this to zero will use the provided context to control the duration. PollingDuration time.Duration // RetryAttempts sets the default number of retry attempts for client. diff --git a/version/version.go b/version/version.go index 94de7c51e..180bbbbf2 100644 --- a/version/version.go +++ b/version/version.go @@ -20,7 +20,7 @@ import ( ) // Number contains the semantic version of this SDK. -const Number = "v10.15.5" +const Number = "v11.0.0" var ( userAgent = fmt.Sprintf("Go/%s (%s-%s) go-autorest/%s",