Skip to content

Commit

Permalink
v13.3.0 (#479)
Browse files Browse the repository at this point in the history
* Deserialize additionalInfo in ARM error

* Allow a new authorizer to be created from a configuration file by specifying a resource instead of a base url.

This enables resource like KeyVault and Container Registry to use an authorizer configured from a configuration file.

* [WIP] Using the Context from the timeout if provided (#315)

* Using the timeout from the context if available

- Makes PollingDuration optional

* Renaming the registration start time

* Making PollingDuration not a pointer

* fixing a broken reference

* Add NewAuthorizerFromCli method which uses Azure CLI to obtain a token for the currently logged in user, for  local development scenarios. (#316)

* Adding User assigned identity support for the MSIConfig authorizor (#332)

* Adding ByteSlicePtr (#399)

* Adding a new `WithXML` method (#402)

* Add HTTP status code response helpers (#403)

Added IsHTTPStatus() and HasHTTPStatus() methods to autorest.Response

* adding a new preparer for `MERGE` used in the Storage API's (#406)

* New Preparer/Responder for `Unmarshalling Bytes` (#407)

* New Preparer: WithBytes

* New Responder: `ByUnmarshallingBytes`

* Reusing the bytes, rather than copying them

* Fixing the broken test / switching to read the bytes directly

* Support HTTP-Date in Retry-After header (#410)

RFC specifies Retry-After header can be integer value expressing seconds
or an HTTP-Date indicating when to try again.
Removed superfluous check for HTTP status code.

* Add support for multi-tenant authentication (#412)

* Add support for multi-tenant authentication

Support for multi-tenant via x-ms-authorization-auxiliary header has
been added for client credentials with secret scenario; this basically
bundles multiple OAuthConfig and ServicePrincipalToken types into
corresponding MultiTenant* types along with a new authorizer that adds
the primary and auxiliary token headers to the reqest.
The authenticaion helpers have been updated to support this scenario; if
environment var AZURE_AUXILIARY_TENANT_IDS is set with a semicolon
delimited list of tenants the multi-tenant codepath will kick in to
create the appropriate authorizer.

* feedback

* rename Options to OAuthOptions (#415)

* Support custom SendDecorator chains via context (#417)

* Support custom SendDecorator chains via context

Added `autorest.WithSendDecorators` and `autorest.GetSendDecorators` for
adding and retrieving a custom chain of SendDecorators to the provided
context.
Added `autorest.DoRetryForStatusCodesWithCap` and
`autorest.DelayForBackoffWithCap` to enforce an upper bound on the
duration between retries.
Fixed up some code comments.

* small refactor based on PR feedback

* remove some changes for dev branch

* merge master into dev (#427)

* v12.3.0 (#418)

* Deserialize additionalInfo in ARM error

* Allow a new authorizer to be created from a configuration file by specifying a resource instead of a base url.

This enables resource like KeyVault and Container Registry to use an authorizer configured from a configuration file.

* [WIP] Using the Context from the timeout if provided (#315)

* Using the timeout from the context if available

- Makes PollingDuration optional

* Renaming the registration start time

* Making PollingDuration not a pointer

* fixing a broken reference

* Add NewAuthorizerFromCli method which uses Azure CLI to obtain a token for the currently logged in user, for  local development scenarios. (#316)

* Adding User assigned identity support for the MSIConfig authorizor (#332)

* Adding ByteSlicePtr (#399)

* Adding a new `WithXML` method (#402)

* Add HTTP status code response helpers (#403)

Added IsHTTPStatus() and HasHTTPStatus() methods to autorest.Response

* adding a new preparer for `MERGE` used in the Storage API's (#406)

* New Preparer/Responder for `Unmarshalling Bytes` (#407)

* New Preparer: WithBytes

* New Responder: `ByUnmarshallingBytes`

* Reusing the bytes, rather than copying them

* Fixing the broken test / switching to read the bytes directly

* Support HTTP-Date in Retry-After header (#410)

RFC specifies Retry-After header can be integer value expressing seconds
or an HTTP-Date indicating when to try again.
Removed superfluous check for HTTP status code.

* Add support for multi-tenant authentication (#412)

* Add support for multi-tenant authentication

Support for multi-tenant via x-ms-authorization-auxiliary header has
been added for client credentials with secret scenario; this basically
bundles multiple OAuthConfig and ServicePrincipalToken types into
corresponding MultiTenant* types along with a new authorizer that adds
the primary and auxiliary token headers to the reqest.
The authenticaion helpers have been updated to support this scenario; if
environment var AZURE_AUXILIARY_TENANT_IDS is set with a semicolon
delimited list of tenants the multi-tenant codepath will kick in to
create the appropriate authorizer.

* feedback

* rename Options to OAuthOptions (#415)

* Support custom SendDecorator chains via context (#417)

* Support custom SendDecorator chains via context

Added `autorest.WithSendDecorators` and `autorest.GetSendDecorators` for
adding and retrieving a custom chain of SendDecorators to the provided
context.
Added `autorest.DoRetryForStatusCodesWithCap` and
`autorest.DelayForBackoffWithCap` to enforce an upper bound on the
duration between retries.
Fixed up some code comments.

* small refactor based on PR feedback

* remove some changes for dev branch

* v12.3.0

* add yaml file for azure devops CI (#419)

* add status badge for azure devops CI (#420)

* enable build and test on linux (#421)

* enable build and test on linux

* fail on first error and use portable std*

* update test to run on devops

* Refactor azure devops pipeline (#422)

Break monolithic script into separate scripts with useful names.
Moved formatting checks to the end with succeededOrFailed conditions.

* remove travis artifacts (#423)

* remove unnecessary trigger section from devops (#424)

* Use accessTokens.json from AZURE_CONFIG_DIR if AZURE_ACCESS_TOKEN_FILE is not set before falling back on ~/.azure/ (#471)

* support for parsing error messages from xml responses (#465)

* support for parsing error messages from xml responses

* fixing the linting

* removed some duplicate code

* fix bug introduced in refactoring

* added XML test and fixed bug it uncovered

* fix godoc comment for methods that are safe for concurrent use (#475)

* New Authorizers for Azure Storage (#416)

* Authorizers for Blob, File, Queue and Table Storage

* Adding a SharedKey authorizer

* refactor based on existing storage implementation

* add missing storage emulator account name

* replace hard-coded strings with constants

* changed to by-ref

* Adding a new Authorizer for SAS Token Authentication (#478)

* Adding a new Authorizer for SAS Token Authentication

This commit introduces a new Authorizer for authenticating with
Blob Storage using a SAS Token

```
$ go test -v ./autorest/ -run="TestSas"
=== RUN   TestSasNewSasAuthorizerEmptyToken
--- PASS: TestSasNewSasAuthorizerEmptyToken (0.00s)
=== RUN   TestSasNewSasAuthorizerEmptyTokenWithWhitespace
--- PASS: TestSasNewSasAuthorizerEmptyTokenWithWhitespace (0.00s)
=== RUN   TestSasNewSasAuthorizerValidToken
--- PASS: TestSasNewSasAuthorizerValidToken (0.00s)
=== RUN   TestSasAuthorizerRequest
--- PASS: TestSasAuthorizerRequest (0.00s)
    authorization_sas_test.go:76: [DEBUG] Testing Case "empty querystring without a prefix"..
    authorization_sas_test.go:76: [DEBUG] Testing Case "empty querystring with a prefix"..
    authorization_sas_test.go:76: [DEBUG] Testing Case "existing querystring without a prefix"..
    authorization_sas_test.go:76: [DEBUG] Testing Case "existing querystring with a prefix"..
PASS
ok  	github.com/Azure/go-autorest/autorest	0.011s
```

* minor clean-up

* token: support for a custom refresh func (#476)

* token: support for a custom refresh func

* pass closures by value

* minor clean-up

* v13.3.0
  • Loading branch information
jhendrixMSFT authored Oct 28, 2019
1 parent 740293c commit 3492b2a
Show file tree
Hide file tree
Showing 13 changed files with 751 additions and 27 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# CHANGELOG

## v13.3.0

### New Features

- Added support for shared key and shared access signature token authorization.
- `autorest.NewSharedKeyAuthorizer()` and dependent types.
- `autorest.NewSASTokenAuthorizer()` and dependent types.
- Added `ServicePrincipalToken.SetCustomRefresh()` so a custom refresh function can be invoked when a token has expired.

### Bug Fixes

- Fixed `cli.AccessTokensPath()` to respect `AZURE_CONFIG_DIR` when set.
- Support parsing error messages in XML responses.

## v13.2.0

### New Features
Expand Down
34 changes: 26 additions & 8 deletions autorest/adal/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ type RefresherWithContext interface {
// a successful token refresh
type TokenRefreshCallback func(Token) error

// TokenRefresh is a type representing a custom callback to refresh a token
type TokenRefresh func(ctx context.Context, resource string) (*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 {
Expand Down Expand Up @@ -344,10 +347,11 @@ func (secret ServicePrincipalAuthorizationCodeSecret) MarshalJSON() ([]byte, err

// ServicePrincipalToken encapsulates a Token created for a Service Principal.
type ServicePrincipalToken struct {
inner servicePrincipalToken
refreshLock *sync.RWMutex
sender Sender
refreshCallbacks []TokenRefreshCallback
inner servicePrincipalToken
refreshLock *sync.RWMutex
sender Sender
customRefreshFunc TokenRefresh
refreshCallbacks []TokenRefreshCallback
// MaxMSIRefreshAttempts is the maximum number of attempts to refresh an MSI token.
MaxMSIRefreshAttempts int
}
Expand All @@ -362,6 +366,11 @@ func (spt *ServicePrincipalToken) SetRefreshCallbacks(callbacks []TokenRefreshCa
spt.refreshCallbacks = callbacks
}

// SetCustomRefreshFunc sets a custom refresh function used to refresh the token.
func (spt *ServicePrincipalToken) SetCustomRefreshFunc(customRefreshFunc TokenRefresh) {
spt.customRefreshFunc = customRefreshFunc
}

// MarshalJSON implements the json.Marshaler interface.
func (spt ServicePrincipalToken) MarshalJSON() ([]byte, error) {
return json.Marshal(spt.inner)
Expand Down Expand Up @@ -786,27 +795,27 @@ func (spt *ServicePrincipalToken) InvokeRefreshCallbacks(token Token) error {
}

// Refresh obtains a fresh token for the Service Principal.
// This method is not safe for concurrent use and should be syncrhonized.
// This method is safe for concurrent use.
func (spt *ServicePrincipalToken) Refresh() error {
return spt.RefreshWithContext(context.Background())
}

// RefreshWithContext obtains a fresh token for the Service Principal.
// This method is not safe for concurrent use and should be syncrhonized.
// This method is safe for concurrent use.
func (spt *ServicePrincipalToken) RefreshWithContext(ctx context.Context) error {
spt.refreshLock.Lock()
defer spt.refreshLock.Unlock()
return spt.refreshInternal(ctx, spt.inner.Resource)
}

// RefreshExchange refreshes the token, but for a different resource.
// This method is not safe for concurrent use and should be syncrhonized.
// This method is safe for concurrent use.
func (spt *ServicePrincipalToken) RefreshExchange(resource string) error {
return spt.RefreshExchangeWithContext(context.Background(), resource)
}

// RefreshExchangeWithContext refreshes the token, but for a different resource.
// This method is not safe for concurrent use and should be syncrhonized.
// This method is safe for concurrent use.
func (spt *ServicePrincipalToken) RefreshExchangeWithContext(ctx context.Context, resource string) error {
spt.refreshLock.Lock()
defer spt.refreshLock.Unlock()
Expand All @@ -833,6 +842,15 @@ func isIMDS(u url.URL) bool {
}

func (spt *ServicePrincipalToken) refreshInternal(ctx context.Context, resource string) error {
if spt.customRefreshFunc != nil {
token, err := spt.customRefreshFunc(ctx, resource)
if err != nil {
return err
}
spt.inner.Token = *token
return spt.InvokeRefreshCallbacks(spt.inner.Token)
}

req, err := http.NewRequest(http.MethodPost, spt.inner.OauthConfig.TokenEndpoint.String(), nil)
if err != nil {
return fmt.Errorf("adal: Failed to build the refresh request. Error = '%v'", err)
Expand Down
38 changes: 38 additions & 0 deletions autorest/adal/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,24 @@ func TestServicePrincipalTokenSetAutoRefresh(t *testing.T) {
}
}

func TestServicePrincipalTokenSetCustomRefreshFunc(t *testing.T) {
spt := newServicePrincipalToken()

var refreshFunc TokenRefresh = func(context context.Context, resource string) (*Token, error) {
return nil, nil
}

if spt.customRefreshFunc != nil {
t.Fatalf("adal: ServicePrincipalToken#SetCustomRefreshFunc had a default custom refresh func when it shouldn't")
}

spt.SetCustomRefreshFunc(refreshFunc)

if spt.customRefreshFunc == nil {
t.Fatalf("adal: ServicePrincipalToken#SetCustomRefreshFunc didn't have a refresh func")
}
}

func TestServicePrincipalTokenSetRefreshWithin(t *testing.T) {
spt := newServicePrincipalToken()

Expand All @@ -123,6 +141,26 @@ func TestServicePrincipalTokenSetSender(t *testing.T) {
}
}

func TestServicePrincipalTokenRefreshUsesCustomRefreshFunc(t *testing.T) {
spt := newServicePrincipalToken()

called := false
var refreshFunc TokenRefresh = func(context context.Context, resource string) (*Token, error) {
called = true
return &Token{}, nil
}
spt.SetCustomRefreshFunc(refreshFunc)
if called {
t.Fatalf("adal: ServicePrincipalToken#refreshInternal called the refresh function prior to refreshing")
}

spt.refreshInternal(context.Background(), "https://example.com")

if !called {
t.Fatalf("adal: ServicePrincipalToken#refreshInternal didn't call the refresh function")
}
}

func TestServicePrincipalTokenRefreshUsesPOST(t *testing.T) {
spt := newServicePrincipalToken()

Expand Down
67 changes: 67 additions & 0 deletions autorest/authorization_sas.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package autorest

// Copyright 2017 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import (
"fmt"
"net/http"
"strings"
)

// SASTokenAuthorizer implements an authorization for SAS Token Authentication
// this can be used for interaction with Blob Storage Endpoints
type SASTokenAuthorizer struct {
sasToken string
}

// NewSASTokenAuthorizer creates a SASTokenAuthorizer using the given credentials
func NewSASTokenAuthorizer(sasToken string) (*SASTokenAuthorizer, error) {
if strings.TrimSpace(sasToken) == "" {
return nil, fmt.Errorf("sasToken cannot be empty")
}

token := sasToken
if strings.HasPrefix(sasToken, "?") {
token = strings.TrimPrefix(sasToken, "?")
}

return &SASTokenAuthorizer{
sasToken: token,
}, nil
}

// WithAuthorization returns a PrepareDecorator that adds a shared access signature token to the
// URI's query parameters. This can be used for the Blob, Queue, and File Services.
//
// See https://docs.microsoft.com/en-us/rest/api/storageservices/delegate-access-with-shared-access-signature
func (sas *SASTokenAuthorizer) WithAuthorization() PrepareDecorator {
return func(p Preparer) Preparer {
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
r, err := p.Prepare(r)
if err != nil {
return r, err
}

if r.URL.RawQuery != "" {
r.URL.RawQuery = fmt.Sprintf("%s&%s", r.URL.RawQuery, sas.sasToken)
} else {
r.URL.RawQuery = sas.sasToken
}

r.RequestURI = r.URL.String()
return Prepare(r)
})
}
}
113 changes: 113 additions & 0 deletions autorest/authorization_sas_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package autorest

// Copyright 2017 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import (
"net/http"
"net/url"
"testing"
)

func TestSasNewSasAuthorizerEmptyToken(t *testing.T) {
auth, err := NewSASTokenAuthorizer("")
if err == nil {
t.Fatalf("azure: SASTokenAuthorizer#NewSASTokenAuthorizer didn't return an error")
}

if auth != nil {
t.Fatalf("azure: SASTokenAuthorizer#NewSASTokenAuthorizer returned an authorizer")
}
}

func TestSasNewSasAuthorizerEmptyTokenWithWhitespace(t *testing.T) {
auth, err := NewSASTokenAuthorizer(" ")
if err == nil {
t.Fatalf("azure: SASTokenAuthorizer#NewSASTokenAuthorizer didn't return an error")
}

if auth != nil {
t.Fatalf("azure: SASTokenAuthorizer#NewSASTokenAuthorizer returned an authorizer")
}
}

func TestSasNewSasAuthorizerValidToken(t *testing.T) {
auth, err := NewSASTokenAuthorizer("abc123")
if err != nil {
t.Fatalf("azure: SASTokenAuthorizer#NewSASTokenAuthorizer returned an error")
}

if auth == nil {
t.Fatalf("azure: SASTokenAuthorizer#NewSASTokenAuthorizer didn't return an authorizer")
}
}

func TestSasAuthorizerRequest(t *testing.T) {
testData := []struct {
name string
token string
input string
expected string
}{
{
name: "empty querystring without a prefix",
token: "abc123",
input: "https://example.com/foo/bar",
expected: "https://example.com/foo/bar?abc123",
},
{
name: "empty querystring with a prefix",
token: "?abc123",
input: "https://example.com/foo/bar",
expected: "https://example.com/foo/bar?abc123",
},
{
name: "existing querystring without a prefix",
token: "abc123",
input: "https://example.com/foo/bar?hello=world",
expected: "https://example.com/foo/bar?hello=world&abc123",
},
{
name: "existing querystring with a prefix",
token: "?abc123",
input: "https://example.com/foo/bar?hello=world",
expected: "https://example.com/foo/bar?hello=world&abc123",
},
}

for _, v := range testData {
t.Logf("[DEBUG] Testing Case %q..", v.name)
auth, err := NewSASTokenAuthorizer(v.token)
if err != nil {
t.Fatalf("azure: SASTokenAuthorizer#WithAuthorization expected %q but got an error", v.expected)
}
url, _ := url.ParseRequestURI(v.input)
httpReq := &http.Request{
URL: url,
}

req, err := Prepare(httpReq, auth.WithAuthorization())
if err != nil {
t.Fatalf("azure: SASTokenAuthorizer#WithAuthorization returned an error (%v)", err)
}

if req.RequestURI != v.expected {
t.Fatalf("azure: SASTokenAuthorizer#WithAuthorization failed to set QueryString header - got %q but expected %q", req.RequestURI, v.expected)
}

if req.Header.Get(http.CanonicalHeaderKey("Authorization")) != "" {
t.Fatal("azure: SASTokenAuthorizer#WithAuthorization set an Authorization header when it shouldn't!")
}
}
}
Loading

0 comments on commit 3492b2a

Please sign in to comment.