From 2e08a55e495c9aa767678bfa93bcdf051d6b8042 Mon Sep 17 00:00:00 2001 From: Jin Soon Lim Date: Wed, 13 Jun 2018 16:34:30 -0700 Subject: [PATCH 01/29] Deserialize additionalInfo in ARM error --- autorest/azure/azure.go | 48 ++++++++++++++++++++++-------------- autorest/azure/azure_test.go | 27 ++++++++++++++++---- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/autorest/azure/azure.go b/autorest/azure/azure.go index a702ffe75..3a0a439ff 100644 --- a/autorest/azure/azure.go +++ b/autorest/azure/azure.go @@ -44,11 +44,12 @@ const ( // ServiceError encapsulates the error response from an Azure service. // It adhears to the OData v4 specification for error responses. type ServiceError struct { - Code string `json:"code"` - Message string `json:"message"` - Target *string `json:"target"` - Details []map[string]interface{} `json:"details"` - InnerError map[string]interface{} `json:"innererror"` + Code string `json:"code"` + Message string `json:"message"` + Target *string `json:"target"` + Details []map[string]interface{} `json:"details"` + InnerError map[string]interface{} `json:"innererror"` + AdditionalInfo []map[string]interface{} `json:"additionalInfo"` } func (se ServiceError) Error() string { @@ -74,6 +75,14 @@ func (se ServiceError) Error() string { result += fmt.Sprintf(" InnerError=%v", string(d)) } + if se.AdditionalInfo != nil { + d, err := json.Marshal(se.AdditionalInfo) + if err != nil { + result += fmt.Sprintf(" AdditionalInfo=%v", se.AdditionalInfo) + } + result += fmt.Sprintf(" AdditionalInfo=%v", string(d)) + } + return result } @@ -86,44 +95,47 @@ func (se *ServiceError) UnmarshalJSON(b []byte) error { // http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793091 type serviceError1 struct { - Code string `json:"code"` - Message string `json:"message"` - Target *string `json:"target"` - Details []map[string]interface{} `json:"details"` - InnerError map[string]interface{} `json:"innererror"` + Code string `json:"code"` + Message string `json:"message"` + Target *string `json:"target"` + Details []map[string]interface{} `json:"details"` + InnerError map[string]interface{} `json:"innererror"` + AdditionalInfo []map[string]interface{} `json:"additionalInfo"` } type serviceError2 struct { - Code string `json:"code"` - Message string `json:"message"` - Target *string `json:"target"` - Details map[string]interface{} `json:"details"` - InnerError map[string]interface{} `json:"innererror"` + Code string `json:"code"` + Message string `json:"message"` + Target *string `json:"target"` + Details map[string]interface{} `json:"details"` + InnerError map[string]interface{} `json:"innererror"` + AdditionalInfo []map[string]interface{} `json:"additionalInfo"` } se1 := serviceError1{} err := json.Unmarshal(b, &se1) if err == nil { - se.populate(se1.Code, se1.Message, se1.Target, se1.Details, se1.InnerError) + se.populate(se1.Code, se1.Message, se1.Target, se1.Details, se1.InnerError, se1.AdditionalInfo) return nil } se2 := serviceError2{} err = json.Unmarshal(b, &se2) if err == nil { - se.populate(se2.Code, se2.Message, se2.Target, nil, se2.InnerError) + se.populate(se2.Code, se2.Message, se2.Target, nil, se2.InnerError, se2.AdditionalInfo) se.Details = append(se.Details, se2.Details) return nil } return err } -func (se *ServiceError) populate(code, message string, target *string, details []map[string]interface{}, inner map[string]interface{}) { +func (se *ServiceError) populate(code, message string, target *string, details []map[string]interface{}, inner map[string]interface{}, additional []map[string]interface{}) { se.Code = code se.Message = message se.Target = target se.Details = details se.InnerError = inner + se.AdditionalInfo = additional } // RequestError describes an error response returned by Azure service. diff --git a/autorest/azure/azure_test.go b/autorest/azure/azure_test.go index 612e3a674..a99ccae7f 100644 --- a/autorest/azure/azure_test.go +++ b/autorest/azure/azure_test.go @@ -241,9 +241,10 @@ func TestWithErrorUnlessStatusCode_FoundAzureFullError(t *testing.T) { "code": "InternalError", "message": "Azure is having trouble right now.", "target": "target1", - "details": [{"code": "conflict1", "message":"error message1"}, + "details": [{"code": "conflict1", "message":"error message1"}, {"code": "conflict2", "message":"error message2"}], - "innererror": { "customKey": "customValue" } + "innererror": { "customKey": "customValue" }, + "additionalInfo": [{"type": "someErrorType", "info": {"someProperty": "someValue"}}] } }` uuid := "71FDB9F4-5E49-4C12-B266-DE7B4FD999A6" @@ -287,6 +288,11 @@ func TestWithErrorUnlessStatusCode_FoundAzureFullError(t *testing.T) { t.Fatalf("azure: inner error is not unmarshaled properly") } + a, _ := json.Marshal(azErr.ServiceError.AdditionalInfo) + if string(a) != `[{"info":{"someProperty":"someValue"},"type":"someErrorType"}]` { + t.Fatalf("azure: error additional info is not unmarshaled properly") + } + if expected := http.StatusInternalServerError; azErr.StatusCode != expected { t.Fatalf("azure: got wrong StatusCode=%v Expected=%d", azErr.StatusCode, expected) } @@ -370,7 +376,8 @@ func TestWithErrorUnlessStatusCode_UnwrappedError(t *testing.T) { "target": "target1", "details": [{"code": "conflict1", "message":"error message1"}, {"code": "conflict2", "message":"error message2"}], - "innererror": { "customKey": "customValue" } + "innererror": { "customKey": "customValue" }, + "additionalInfo": [{"type": "someErrorType", "info": {"someProperty": "someValue"}}] }` uuid := "71FDB9F4-5E49-4C12-B266-DE7B4FD999A6" r := mocks.NewResponseWithContent(j) @@ -435,6 +442,15 @@ func TestWithErrorUnlessStatusCode_UnwrappedError(t *testing.T) { t.Fail() } + expectedServiceErrorAdditionalInfo := `[{"info":{"someProperty":"someValue"},"type":"someErrorType"}]` + if azErr.ServiceError.AdditionalInfo == nil { + t.Logf("`ServiceError.AdditionalInfo` was nil when it should have been %q", expectedServiceErrorAdditionalInfo) + t.Fail() + } else if additionalInfo, _ := json.Marshal(azErr.ServiceError.AdditionalInfo); expectedServiceErrorAdditionalInfo != string(additionalInfo) { + t.Logf("Additional info was not unmarshaled properly.\n\tgot: %q\n\twant: %q", string(additionalInfo), expectedServiceErrorAdditionalInfo) + t.Fail() + } + // the error body should still be there defer r.Body.Close() b, err := ioutil.ReadAll(r.Body) @@ -454,7 +470,8 @@ func TestRequestErrorString_WithError(t *testing.T) { "message": "Conflict", "target": "target1", "details": [{"code": "conflict1", "message":"error message1"}], - "innererror": { "customKey": "customValue" } + "innererror": { "customKey": "customValue" }, + "additionalInfo": [{"type": "someErrorType", "info": {"someProperty": "someValue"}}] } }` uuid := "71FDB9F4-5E49-4C12-B266-DE7B4FD999A6" @@ -472,7 +489,7 @@ func TestRequestErrorString_WithError(t *testing.T) { t.Fatalf("azure: returned nil error for proper error response") } azErr, _ := err.(*RequestError) - expected := "autorest/azure: Service returned an error. Status=500 Code=\"InternalError\" Message=\"Conflict\" Target=\"target1\" Details=[{\"code\":\"conflict1\",\"message\":\"error message1\"}] InnerError={\"customKey\":\"customValue\"}" + expected := "autorest/azure: Service returned an error. Status=500 Code=\"InternalError\" Message=\"Conflict\" Target=\"target1\" Details=[{\"code\":\"conflict1\",\"message\":\"error message1\"}] InnerError={\"customKey\":\"customValue\"} AdditionalInfo=[{\"info\":{\"someProperty\":\"someValue\"},\"type\":\"someErrorType\"}]" if expected != azErr.Error() { t.Fatalf("azure: send wrong RequestError.\nexpected=%v\ngot=%v", expected, azErr.Error()) } From 60d22e39483addd6e66463676264d3bd4a87f7a5 Mon Sep 17 00:00:00 2001 From: Nick Muller Date: Tue, 4 Sep 2018 15:42:46 +0200 Subject: [PATCH 02/29] 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. --- .gitignore | 1 + autorest/azure/auth/auth.go | 42 +++++++++++++++++++++++--------- autorest/azure/auth/auth_test.go | 23 ++++++++++++++++- 3 files changed, 53 insertions(+), 13 deletions(-) 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/autorest/azure/auth/auth.go b/autorest/azure/auth/auth.go index a14b87900..2af64896b 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("auth file not found. 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() } } From 07f5297001c446b8855f356fad96077f1f119862 Mon Sep 17 00:00:00 2001 From: Tom Harvey Date: Sat, 29 Sep 2018 06:46:21 +1000 Subject: [PATCH 03/29] [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 --- autorest/adal/sender.go | 2 +- autorest/azure/async.go | 14 +++++++++++--- autorest/azure/rp.go | 6 +++--- autorest/client.go | 1 + 4 files changed, 16 insertions(+), 7 deletions(-) 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/azure/async.go b/autorest/azure/async.go index cda1e180a..d3dc73919 100644 --- a/autorest/azure/async.go +++ b/autorest/azure/async.go @@ -164,9 +164,17 @@ func (f Future) WaitForCompletion(ctx context.Context, client autorest.Client) e // running operation has completed, the provided context is cancelled, or the client's // 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() +func (f *Future) WaitForCompletionRef(inputCtx context.Context, client autorest.Client) error { + + var ctx context.Context + if d := client.PollingDuration; d != 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(inputCtx, d) + defer cancel() + } else { + ctx = inputCtx + } + done, err := f.Done(client) for attempts := 0; !done; done, err = f.Done(client) { if attempts >= client.RetryAttempts { 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 4e92dcad0..95420a9da 100644 --- a/autorest/client.go +++ b/autorest/client.go @@ -153,6 +153,7 @@ type Client struct { PollingDelay time.Duration // PollingDuration sets the maximum polling time after which an error is returned. + // if zero, the timeout from the Context is used PollingDuration time.Duration // RetryAttempts sets the default number of retry attempts for client. From 47d499b27bc8e9bf21a83cb30b80a060d7062829 Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 4 Oct 2018 23:53:12 +0200 Subject: [PATCH 04/29] Add NewAuthorizerFromCli method which uses Azure CLI to obtain a token for the currently logged in user, for local development scenarios. (#316) --- autorest/azure/auth/auth.go | 30 ++++++++++++++++++++ autorest/azure/cli/token.go | 56 +++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/autorest/azure/auth/auth.go b/autorest/azure/auth/auth.go index 2af64896b..90d677ea1 100644 --- a/autorest/azure/auth/auth.go +++ b/autorest/azure/auth/auth.go @@ -31,6 +31,7 @@ import ( "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/adal" "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/azure/cli" "github.com/dimchansky/utfbom" "golang.org/x/crypto/pkcs12" ) @@ -167,6 +168,35 @@ func NewAuthorizerFromFileWithResource(resource string) (autorest.Authorizer, er return autorest.NewBearerAuthorizer(spToken), nil } +// NewAuthorizerFromCLI creates an Authorizer configured from Azure CLI 2.0 for local development scenarios. +func NewAuthorizerFromCLI() (autorest.Authorizer, error) { + settings, err := getAuthenticationSettings() + if err != nil { + return nil, err + } + + if settings.resource == "" { + settings.resource = settings.environment.ResourceManagerEndpoint + } + + return NewAuthorizerFromCLIWithResource(settings.resource) +} + +// NewAuthorizerFromCLIWithResource creates an Authorizer configured from Azure CLI 2.0 for local development scenarios. +func NewAuthorizerFromCLIWithResource(resource string) (autorest.Authorizer, error) { + token, err := cli.GetTokenFromCLI(resource) + if err != nil { + return nil, err + } + + adalToken, err := token.ToADALToken() + if err != nil { + return nil, err + } + + return autorest.NewBearerAuthorizer(&adalToken), nil +} + func getAuthFile() (*file, error) { fileLocation := os.Getenv("AZURE_AUTH_LOCATION") if fileLocation == "" { diff --git a/autorest/azure/cli/token.go b/autorest/azure/cli/token.go index 83b81c34b..40c79b73a 100644 --- a/autorest/azure/cli/token.go +++ b/autorest/azure/cli/token.go @@ -15,9 +15,13 @@ package cli // limitations under the License. import ( + "bytes" "encoding/json" "fmt" "os" + "os/exec" + "regexp" + "runtime" "strconv" "time" @@ -112,3 +116,55 @@ func LoadTokens(path string) ([]Token, error) { return tokens, nil } + +// GetTokenFromCLI gets a token using Azure CLI 2.0 for local development scenarios. +func GetTokenFromCLI(resource string) (*Token, error) { + // This is the path that a developer can set to tell this class what the install path for Azure CLI is. + const azureCLIPath = "AzureCLIPath" + + // 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. + azureCLIDefaultPathWindows := fmt.Sprintf("%s\\Microsoft SDKs\\Azure\\CLI2\\wbin; %s\\Microsoft SDKs\\Azure\\CLI2\\wbin", os.Getenv("ProgramFiles(x86)"), os.Getenv("ProgramFiles")) + + // Default path for non-Windows. + const azureCLIDefaultPath = "/usr/bin:/usr/local/bin" + + // Validate resource, since it gets sent as a command line argument to Azure CLI + const invalidResourceErrorTemplate = "Resource %s is not in expected format. Only alphanumeric characters, [dot], [colon], [hyphen], and [forward slash] are allowed." + match, err := regexp.MatchString("^[0-9a-zA-Z-.:/]+$", resource) + if err != nil { + return nil, err + } + if !match { + return nil, fmt.Errorf(invalidResourceErrorTemplate, resource) + } + + // Execute Azure CLI to get token + var cliCmd *exec.Cmd + if runtime.GOOS == "windows" { + cliCmd = exec.Command(fmt.Sprintf("%s\\system32\\cmd.exe", os.Getenv("windir"))) + cliCmd.Env = os.Environ() + cliCmd.Env = append(cliCmd.Env, fmt.Sprintf("PATH=%s;%s", os.Getenv(azureCLIPath), azureCLIDefaultPathWindows)) + cliCmd.Args = append(cliCmd.Args, "/c") + } else { + cliCmd = exec.Command(os.Getenv("SHELL")) + cliCmd.Env = os.Environ() + cliCmd.Env = append(cliCmd.Env, fmt.Sprintf("PATH=%s:%s", os.Getenv(azureCLIPath), azureCLIDefaultPath)) + } + cliCmd.Args = append(cliCmd.Args, "az", "account", "get-access-token", "-o", "json", "--resource", resource) + + var stderr bytes.Buffer + cliCmd.Stderr = &stderr + + output, err := cliCmd.Output() + if err != nil { + return nil, fmt.Errorf("Invoking Azure CLI failed with the following error: %s", stderr.String()) + } + + tokenResponse := Token{} + err = json.Unmarshal(output, &tokenResponse) + if err != nil { + return nil, err + } + + return &tokenResponse, err +} From e3e073af1cf43993e08a88def42a62633c5f8b34 Mon Sep 17 00:00:00 2001 From: Sam Kreter Date: Fri, 19 Oct 2018 12:24:15 -0700 Subject: [PATCH 05/29] Adding User assigned identity support for the MSIConfig authorizor (#332) --- autorest/azure/auth/auth.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/autorest/azure/auth/auth.go b/autorest/azure/auth/auth.go index dcd232f6e..29b8da4da 100644 --- a/autorest/azure/auth/auth.go +++ b/autorest/azure/auth/auth.go @@ -483,9 +483,17 @@ func (mc MSIConfig) Authorizer() (autorest.Authorizer, error) { return nil, err } - spToken, err := adal.NewServicePrincipalTokenFromMSI(msiEndpoint, mc.Resource) - if err != nil { - return nil, fmt.Errorf("failed to get oauth token from MSI: %v", err) + var spToken *adal.ServicePrincipalToken + if mc.ClientID == "" { + spToken, err = adal.NewServicePrincipalTokenFromMSI(msiEndpoint, mc.Resource) + if err != nil { + return nil, fmt.Errorf("failed to get oauth token from MSI: %v", err) + } + } else { + spToken, err = adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, mc.Resource, mc.ClientID) + if err != nil { + return nil, fmt.Errorf("failed to get oauth token from MSI for user assigned identity: %v", err) + } } return autorest.NewBearerAuthorizer(spToken), nil From b3989e6e04ffa20fa4583f1d35a164f1b6c28d3c Mon Sep 17 00:00:00 2001 From: Delyan Raychev <49918230+draychev@users.noreply.github.com> Date: Mon, 20 May 2019 09:46:19 -0700 Subject: [PATCH 06/29] Adding ByteSlicePtr (#399) --- autorest/to/convert.go | 5 +++++ autorest/to/convert_test.go | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/autorest/to/convert.go b/autorest/to/convert.go index fdda2ce1a..86694bd25 100644 --- a/autorest/to/convert.go +++ b/autorest/to/convert.go @@ -145,3 +145,8 @@ func Float64(i *float64) float64 { func Float64Ptr(i float64) *float64 { return &i } + +// ByteSlicePtr returns a pointer to the passed byte slice. +func ByteSlicePtr(b []byte) *[]byte { + return &b +} diff --git a/autorest/to/convert_test.go b/autorest/to/convert_test.go index f8177a163..b7b85ed33 100644 --- a/autorest/to/convert_test.go +++ b/autorest/to/convert_test.go @@ -232,3 +232,11 @@ func TestFloat64Ptr(t *testing.T) { v, *Float64Ptr(v)) } } + +func TestByteSlicePtr(t *testing.T) { + v := []byte("bytes") + if out := ByteSlicePtr(v); !reflect.DeepEqual(*out, v) { + t.Fatalf("to: ByteSlicePtr failed to return the correct slice -- expected %v, received %v", + v, *out) + } +} From 6dadc53ec0ab86f6155937c3baa712369527b474 Mon Sep 17 00:00:00 2001 From: Tom Harvey Date: Fri, 31 May 2019 11:03:53 -0500 Subject: [PATCH 07/29] Adding a new `WithXML` method (#402) --- autorest/preparer.go | 23 +++++++++++++++++++++++ autorest/preparer_test.go | 20 ++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/autorest/preparer.go b/autorest/preparer.go index 6d67bd733..aeb60d34e 100644 --- a/autorest/preparer.go +++ b/autorest/preparer.go @@ -17,6 +17,7 @@ package autorest import ( "bytes" "encoding/json" + "encoding/xml" "fmt" "io" "io/ioutil" @@ -377,6 +378,28 @@ func WithJSON(v interface{}) PrepareDecorator { } } +// WithXML returns a PrepareDecorator that encodes the data passed as XML into the body of the +// request and sets the Content-Length header. +func WithXML(v interface{}) PrepareDecorator { + return func(p Preparer) Preparer { + return PreparerFunc(func(r *http.Request) (*http.Request, error) { + r, err := p.Prepare(r) + if err == nil { + b, err := xml.Marshal(v) + if err == nil { + // we have to tack on an XML header + withHeader := xml.Header + string(b) + bytesWithHeader := []byte(withHeader) + + r.ContentLength = int64(len(bytesWithHeader)) + r.Body = ioutil.NopCloser(bytes.NewReader(bytesWithHeader)) + } + } + return r, err + }) + } +} + // WithPath returns a PrepareDecorator that adds the supplied path to the request URL. If the path // is absolute (that is, it begins with a "/"), it replaces the existing path. func WithPath(path string) PrepareDecorator { diff --git a/autorest/preparer_test.go b/autorest/preparer_test.go index b4d19967b..80e52a7ff 100644 --- a/autorest/preparer_test.go +++ b/autorest/preparer_test.go @@ -238,6 +238,26 @@ func ExampleWithJSON() { // Output: Request Body contains {"name":"Rob Pike","age":42} } +// Create a request whose Body is the XML encoding of a structure +func ExampleWithXML() { + t := mocks.T{Name: "Rob Pike", Age: 42} + + r, err := Prepare(&http.Request{}, + WithXML(&t)) + if err != nil { + fmt.Printf("ERROR: %v\n", err) + } + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + fmt.Printf("ERROR: %v\n", err) + } else { + fmt.Printf("Request Body contains %s\n", string(b)) + } + // Output: Request Body contains + // Rob Pike42 +} + // Create a request from a path with escaped parameters func ExampleWithEscapedPathParameters() { params := map[string]interface{}{ From 98c297648b48d4704e5d20087d020d82a170d781 Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Wed, 5 Jun 2019 14:38:30 -0700 Subject: [PATCH 08/29] Add HTTP status code response helpers (#403) Added IsHTTPStatus() and HasHTTPStatus() methods to autorest.Response --- autorest/client.go | 16 ++++++++++++++++ autorest/client_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/autorest/client.go b/autorest/client.go index cfc7ed757..92da6adb2 100644 --- a/autorest/client.go +++ b/autorest/client.go @@ -73,6 +73,22 @@ type Response struct { *http.Response `json:"-"` } +// IsHTTPStatus returns true if the returned HTTP status code matches the provided status code. +// If there was no response (i.e. the underlying http.Response is nil) the return value is false. +func (r Response) IsHTTPStatus(statusCode int) bool { + if r.Response == nil { + return false + } + return r.Response.StatusCode == statusCode +} + +// HasHTTPStatus returns true if the returned HTTP status code matches one of the provided status codes. +// If there was no response (i.e. the underlying http.Response is nil) or not status codes are provided +// the return value is false. +func (r Response) HasHTTPStatus(statusCodes ...int) bool { + return ResponseHasStatusCode(r.Response, statusCodes...) +} + // LoggingInspector implements request and response inspectors that log the full request and // response to a supplied log. type LoggingInspector struct { diff --git a/autorest/client_test.go b/autorest/client_test.go index 9ae7ed608..ca896714d 100644 --- a/autorest/client_test.go +++ b/autorest/client_test.go @@ -435,6 +435,37 @@ func TestCookies(t *testing.T) { } } +func TestResponseIsHTTPStatus(t *testing.T) { + r := Response{} + if r.IsHTTPStatus(http.StatusBadRequest) { + t.Fatal("autorest: expected false for nil response") + } + r.Response = &http.Response{StatusCode: http.StatusOK} + if r.IsHTTPStatus(http.StatusBadRequest) { + t.Fatal("autorest: expected false") + } + if !r.IsHTTPStatus(http.StatusOK) { + t.Fatal("autorest: expected true") + } +} + +func TestResponseHasHTTPStatus(t *testing.T) { + r := Response{} + if r.HasHTTPStatus(http.StatusBadRequest, http.StatusInternalServerError) { + t.Fatal("autorest: expected false for nil response") + } + r.Response = &http.Response{StatusCode: http.StatusAccepted} + if r.HasHTTPStatus(http.StatusBadRequest, http.StatusInternalServerError) { + t.Fatal("autorest: expected false") + } + if !r.HasHTTPStatus(http.StatusOK, http.StatusCreated, http.StatusAccepted) { + t.Fatal("autorest: expected true") + } + if r.HasHTTPStatus() { + t.Fatal("autorest: expected false for no status codes") + } +} + func randomString(n int) string { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" r := rand.New(rand.NewSource(time.Now().UTC().UnixNano())) From ae1b5cfce7302609ba3608dd6c2c80fa10911e85 Mon Sep 17 00:00:00 2001 From: Tom Harvey Date: Wed, 19 Jun 2019 17:37:17 +0200 Subject: [PATCH 09/29] adding a new preparer for `MERGE` used in the Storage API's (#406) --- autorest/preparer.go | 3 +++ autorest/preparer_test.go | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/autorest/preparer.go b/autorest/preparer.go index aeb60d34e..3d972c157 100644 --- a/autorest/preparer.go +++ b/autorest/preparer.go @@ -191,6 +191,9 @@ func AsGet() PrepareDecorator { return WithMethod("GET") } // AsHead returns a PrepareDecorator that sets the HTTP method to HEAD. func AsHead() PrepareDecorator { return WithMethod("HEAD") } +// AsMerge returns a PrepareDecorator that sets the HTTP method to MERGE. +func AsMerge() PrepareDecorator { return WithMethod("MERGE") } + // AsOptions returns a PrepareDecorator that sets the HTTP method to OPTIONS. func AsOptions() PrepareDecorator { return WithMethod("OPTIONS") } diff --git a/autorest/preparer_test.go b/autorest/preparer_test.go index 80e52a7ff..096a7006a 100644 --- a/autorest/preparer_test.go +++ b/autorest/preparer_test.go @@ -468,6 +468,13 @@ func TestAsHead(t *testing.T) { } } +func TestAsMerge(t *testing.T) { + r, _ := Prepare(mocks.NewRequest(), AsMerge()) + if r.Method != "MERGE" { + t.Fatal("autorest: AsMerge failed to set HTTP method header to MERGE") + } +} + func TestAsOptions(t *testing.T) { r, _ := Prepare(mocks.NewRequest(), AsOptions()) if r.Method != "OPTIONS" { From a6c32247a670edc87c768e682931dfe7b7369cfc Mon Sep 17 00:00:00 2001 From: Tom Harvey Date: Wed, 19 Jun 2019 19:34:02 +0200 Subject: [PATCH 10/29] 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 --- autorest/mocks/helpers.go | 13 +++++++++++++ autorest/mocks/mocks.go | 8 ++++++++ autorest/preparer.go | 19 +++++++++++++++++++ autorest/preparer_test.go | 24 ++++++++++++++++++++++++ autorest/responder.go | 19 +++++++++++++++++++ autorest/responder_test.go | 19 +++++++++++++++++++ 6 files changed, 102 insertions(+) diff --git a/autorest/mocks/helpers.go b/autorest/mocks/helpers.go index e2aab19c9..f8b2f8b1a 100644 --- a/autorest/mocks/helpers.go +++ b/autorest/mocks/helpers.go @@ -90,6 +90,19 @@ func NewResponse() *http.Response { return NewResponseWithContent("") } +// NewResponseWithBytes instantiates a new response with the passed bytes as the body content. +func NewResponseWithBytes(input []byte) *http.Response { + return &http.Response{ + Status: "200 OK", + StatusCode: 200, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + Body: NewBodyWithBytes(input), + Request: NewRequest(), + } +} + // NewResponseWithContent instantiates a new response with the passed string as the body content. func NewResponseWithContent(c string) *http.Response { return &http.Response{ diff --git a/autorest/mocks/mocks.go b/autorest/mocks/mocks.go index cc1579e0f..a00a27dd8 100644 --- a/autorest/mocks/mocks.go +++ b/autorest/mocks/mocks.go @@ -37,6 +37,14 @@ func NewBody(s string) *Body { return (&Body{s: s}).reset() } +// NewBodyWithBytes creates a new instance of Body. +func NewBodyWithBytes(b []byte) *Body { + return &Body{ + b: b, + isOpen: true, + } +} + // NewBodyClose creates a new instance of Body. func NewBodyClose(s string) *Body { return &Body{s: s} diff --git a/autorest/preparer.go b/autorest/preparer.go index 3d972c157..6e955a4ba 100644 --- a/autorest/preparer.go +++ b/autorest/preparer.go @@ -229,6 +229,25 @@ func WithBaseURL(baseURL string) PrepareDecorator { } } +// WithBytes returns a PrepareDecorator that takes a list of bytes +// which passes the bytes directly to the body +func WithBytes(input *[]byte) PrepareDecorator { + return func(p Preparer) Preparer { + return PreparerFunc(func(r *http.Request) (*http.Request, error) { + r, err := p.Prepare(r) + if err == nil { + if input == nil { + return r, fmt.Errorf("Input Bytes was nil") + } + + r.ContentLength = int64(len(*input)) + r.Body = ioutil.NopCloser(bytes.NewReader(*input)) + } + return r, err + }) + } +} + // WithCustomBaseURL returns a PrepareDecorator that replaces brace-enclosed keys within the // request base URL (i.e., http.Request.URL) with the corresponding values from the passed map. func WithCustomBaseURL(baseURL string, urlParameters map[string]interface{}) PrepareDecorator { diff --git a/autorest/preparer_test.go b/autorest/preparer_test.go index 096a7006a..3ace6554f 100644 --- a/autorest/preparer_test.go +++ b/autorest/preparer_test.go @@ -163,6 +163,30 @@ func ExampleWithBaseURL_second() { // Output: parse :: missing protocol scheme } +// Create a request whose Body is a byte array +func TestWithBytes(t *testing.T) { + input := []byte{41, 82, 109} + + r, err := Prepare(&http.Request{}, + WithBytes(&input)) + if err != nil { + t.Fatalf("ERROR: %v\n", err) + } + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatalf("ERROR: %v\n", err) + } + + if len(b) != len(input) { + t.Fatalf("Expected the Body to contain %d bytes but got %d", len(input), len(b)) + } + + if !reflect.DeepEqual(b, input) { + t.Fatalf("Body doesn't contain the same bytes: %s (Expected %s)", b, input) + } +} + func ExampleWithCustomBaseURL() { r, err := Prepare(&http.Request{}, WithCustomBaseURL("https://{account}.{service}.core.windows.net/", diff --git a/autorest/responder.go b/autorest/responder.go index a908a0adb..349e1963a 100644 --- a/autorest/responder.go +++ b/autorest/responder.go @@ -153,6 +153,25 @@ func ByClosingIfError() RespondDecorator { } } +// ByUnmarshallingBytes returns a RespondDecorator that copies the Bytes returned in the +// response Body into the value pointed to by v. +func ByUnmarshallingBytes(v *[]byte) RespondDecorator { + return func(r Responder) Responder { + return ResponderFunc(func(resp *http.Response) error { + err := r.Respond(resp) + if err == nil { + bytes, errInner := ioutil.ReadAll(resp.Body) + if errInner != nil { + err = fmt.Errorf("Error occurred reading http.Response#Body - Error = '%v'", errInner) + } else { + *v = bytes + } + } + return err + }) + } +} + // ByUnmarshallingJSON returns a RespondDecorator that decodes a JSON document returned in the // response Body into the value pointed to by v. func ByUnmarshallingJSON(v interface{}) RespondDecorator { diff --git a/autorest/responder_test.go b/autorest/responder_test.go index 4a57b1e3b..1d823ebbc 100644 --- a/autorest/responder_test.go +++ b/autorest/responder_test.go @@ -48,6 +48,25 @@ func ExampleWithErrorUnlessOK() { // Output: GET of https://microsoft.com/a/b/c/ returned HTTP 200 } +func TestByUnmarshallingBytes(t *testing.T) { + expected := []byte("Lorem Ipsum Dolor") + + // we'll create a fixed-sized array here, since that's the expectation + bytes := make([]byte, len(expected)) + + Respond(mocks.NewResponseWithBytes(expected), + ByUnmarshallingBytes(&bytes), + ByClosing()) + + if len(bytes) != len(expected) { + t.Fatalf("Expected Response to be %d bytes but got %d bytes", len(expected), len(bytes)) + } + + if !reflect.DeepEqual(expected, bytes) { + t.Fatalf("Expected Response to be %s but got %s", expected, bytes) + } +} + func ExampleByUnmarshallingJSON() { c := ` { From 851f98c37329c0dc6d31d7450309a0215bf5cb20 Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Thu, 20 Jun 2019 13:50:27 -0700 Subject: [PATCH 11/29] 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. --- autorest/sender.go | 18 +++++++++++++----- autorest/sender_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/autorest/sender.go b/autorest/sender.go index 6665d7c00..2f4b0ddda 100644 --- a/autorest/sender.go +++ b/autorest/sender.go @@ -248,16 +248,24 @@ func DoRetryForStatusCodes(attempts int, backoff time.Duration, codes ...int) Se } } -// DelayWithRetryAfter invokes time.After for the duration specified in the "Retry-After" header in -// responses with status code 429 +// DelayWithRetryAfter invokes time.After for the duration specified in the "Retry-After" header. +// The value of Retry-After can be either the number of seconds or a date in RFC1123 format. +// The function returns true after successfully waiting for the specified duration. If there is +// no Retry-After header or the wait is cancelled the return value is false. func DelayWithRetryAfter(resp *http.Response, cancel <-chan struct{}) bool { if resp == nil { return false } - retryAfter, _ := strconv.Atoi(resp.Header.Get("Retry-After")) - if resp.StatusCode == http.StatusTooManyRequests && retryAfter > 0 { + var dur time.Duration + ra := resp.Header.Get("Retry-After") + if retryAfter, _ := strconv.Atoi(ra); retryAfter > 0 { + dur = time.Duration(retryAfter) * time.Second + } else if t, err := time.Parse(time.RFC1123, ra); err == nil { + dur = t.Sub(time.Now()) + } + if dur > 0 { select { - case <-time.After(time.Duration(retryAfter) * time.Second): + case <-time.After(dur): return true case <-cancel: return false diff --git a/autorest/sender_test.go b/autorest/sender_test.go index 218637394..29bc7e91a 100644 --- a/autorest/sender_test.go +++ b/autorest/sender_test.go @@ -818,6 +818,33 @@ func TestDelayWithRetryAfterWithSuccess(t *testing.T) { } } +func TestDelayWithRetryAfterWithSuccessDateTime(t *testing.T) { + resumeAt := time.Now().Add(2 * time.Second).Round(time.Second) + + client := mocks.NewSender() + resp := mocks.NewResponseWithStatus("503 Service temporarily unavailable", http.StatusServiceUnavailable) + mocks.SetResponseHeader(resp, "Retry-After", resumeAt.Format(time.RFC1123)) + client.AppendResponse(resp) + client.AppendResponse(mocks.NewResponseWithStatus("200 OK", http.StatusOK)) + + r, _ := SendWithSender(client, mocks.NewRequest(), + DoRetryForStatusCodes(1, time.Duration(time.Second), http.StatusServiceUnavailable), + ) + + if time.Now().Before(resumeAt) { + t.Fatal("autorest: DelayWithRetryAfter failed stopped too soon") + } + + Respond(r, + ByDiscardingBody(), + ByClosing()) + + if client.Attempts() != 2 { + t.Fatalf("autorest: Sender#DelayWithRetryAfter -- Got: StatusCode %v in %v attempts; Want: StatusCode 200 OK in 2 attempts -- ", + r.Status, client.Attempts()-1) + } +} + type temporaryError struct { message string } From ef486687f273405a2e6f35ccc64beaefb6e7862e Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Tue, 25 Jun 2019 15:41:31 -0700 Subject: [PATCH 12/29] 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 --- autorest/adal/config.go | 62 +++++++++++++++++++++++++++- autorest/adal/config_test.go | 71 +++++++++++++++++++++++--------- autorest/adal/token.go | 70 +++++++++++++++++++++++++++++++ autorest/adal/token_test.go | 16 +++++++ autorest/authorization.go | 49 ++++++++++++++++++++++ autorest/authorization_test.go | 49 ++++++++++++++++++++++ autorest/azure/auth/auth.go | 31 ++++++++++++-- autorest/azure/auth/auth_test.go | 34 +++++++++++++++ autorest/preparer.go | 7 ++-- 9 files changed, 362 insertions(+), 27 deletions(-) diff --git a/autorest/adal/config.go b/autorest/adal/config.go index 8c83a917f..b486b648e 100644 --- a/autorest/adal/config.go +++ b/autorest/adal/config.go @@ -15,10 +15,15 @@ package adal // limitations under the License. import ( + "errors" "fmt" "net/url" ) +const ( + activeDirectoryEndpointTemplate = "%s/oauth2/%s%s" +) + // OAuthConfig represents the endpoints needed // in OAuth operations type OAuthConfig struct { @@ -60,7 +65,6 @@ func NewOAuthConfigWithAPIVersion(activeDirectoryEndpoint, tenantID string, apiV } api = fmt.Sprintf("?api-version=%s", *apiVersion) } - const activeDirectoryEndpointTemplate = "%s/oauth2/%s%s" u, err := url.Parse(activeDirectoryEndpoint) if err != nil { return nil, err @@ -89,3 +93,59 @@ func NewOAuthConfigWithAPIVersion(activeDirectoryEndpoint, tenantID string, apiV DeviceCodeEndpoint: *deviceCodeURL, }, nil } + +// MultiTenantOAuthConfig provides endpoints for primary and aulixiary tenant IDs. +type MultiTenantOAuthConfig interface { + PrimaryTenant() *OAuthConfig + AuxiliaryTenants() []*OAuthConfig +} + +// Options contains optional OAuthConfig creation arguments. +type Options struct { + APIVersion string +} + +func (c Options) apiVersion() string { + if c.APIVersion != "" { + return fmt.Sprintf("?api-version=%s", c.APIVersion) + } + return "1.0" +} + +// NewMultiTenantOAuthConfig creates an object that support multitenant OAuth configuration. +// See https://docs.microsoft.com/en-us/azure/azure-resource-manager/authenticate-multi-tenant for more information. +func NewMultiTenantOAuthConfig(activeDirectoryEndpoint, primaryTenantID string, auxiliaryTenantIDs []string, options Options) (MultiTenantOAuthConfig, error) { + if len(auxiliaryTenantIDs) == 0 || len(auxiliaryTenantIDs) > 3 { + return nil, errors.New("must specify one to three auxiliary tenants") + } + mtCfg := multiTenantOAuthConfig{ + cfgs: make([]*OAuthConfig, len(auxiliaryTenantIDs)+1), + } + apiVer := options.apiVersion() + pri, err := NewOAuthConfigWithAPIVersion(activeDirectoryEndpoint, primaryTenantID, &apiVer) + if err != nil { + return nil, fmt.Errorf("failed to create OAuthConfig for primary tenant: %v", err) + } + mtCfg.cfgs[0] = pri + for i := range auxiliaryTenantIDs { + aux, err := NewOAuthConfig(activeDirectoryEndpoint, auxiliaryTenantIDs[i]) + if err != nil { + return nil, fmt.Errorf("failed to create OAuthConfig for tenant '%s': %v", auxiliaryTenantIDs[i], err) + } + mtCfg.cfgs[i+1] = aux + } + return mtCfg, nil +} + +type multiTenantOAuthConfig struct { + // first config in the slice is the primary tenant + cfgs []*OAuthConfig +} + +func (m multiTenantOAuthConfig) PrimaryTenant() *OAuthConfig { + return m.cfgs[0] +} + +func (m multiTenantOAuthConfig) AuxiliaryTenants() []*OAuthConfig { + return m.cfgs[1:] +} diff --git a/autorest/adal/config_test.go b/autorest/adal/config_test.go index aad0b5a11..10584ca9c 100644 --- a/autorest/adal/config_test.go +++ b/autorest/adal/config_test.go @@ -15,81 +15,112 @@ package adal // limitations under the License. import ( + "fmt" "testing" ) -func TestNewOAuthConfig(t *testing.T) { - const testActiveDirectoryEndpoint = "https://login.test.com" - const testTenantID = "tenant-id-test" +const TestAuxTenantPrefix = "aux-tenant-test-" + +var ( + TestAuxTenantIDs = []string{TestAuxTenantPrefix + "0", TestAuxTenantPrefix + "1", TestAuxTenantPrefix + "2"} +) - config, err := NewOAuthConfig(testActiveDirectoryEndpoint, testTenantID) +func TestNewOAuthConfig(t *testing.T) { + config, err := NewOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID) if err != nil { t.Fatalf("autorest/adal: Unexpected error while creating oauth configuration for tenant: %v.", err) } - expected := "https://login.test.com/tenant-id-test/oauth2/authorize?api-version=1.0" + expected := fmt.Sprintf("https://login.test.com/%s/oauth2/authorize?api-version=1.0", TestTenantID) if config.AuthorizeEndpoint.String() != expected { t.Fatalf("autorest/adal: Incorrect authorize url for Tenant from Environment. expected(%s). actual(%v).", expected, config.AuthorizeEndpoint) } - expected = "https://login.test.com/tenant-id-test/oauth2/token?api-version=1.0" + expected = fmt.Sprintf("https://login.test.com/%s/oauth2/token?api-version=1.0", TestTenantID) if config.TokenEndpoint.String() != expected { t.Fatalf("autorest/adal: Incorrect authorize url for Tenant from Environment. expected(%s). actual(%v).", expected, config.TokenEndpoint) } - expected = "https://login.test.com/tenant-id-test/oauth2/devicecode?api-version=1.0" + expected = fmt.Sprintf("https://login.test.com/%s/oauth2/devicecode?api-version=1.0", TestTenantID) if config.DeviceCodeEndpoint.String() != expected { t.Fatalf("autorest/adal Incorrect devicecode url for Tenant from Environment. expected(%s). actual(%v).", expected, config.DeviceCodeEndpoint) } } func TestNewOAuthConfigWithAPIVersionNil(t *testing.T) { - const testActiveDirectoryEndpoint = "https://login.test.com" - const testTenantID = "tenant-id-test" - - config, err := NewOAuthConfigWithAPIVersion(testActiveDirectoryEndpoint, testTenantID, nil) + config, err := NewOAuthConfigWithAPIVersion(TestActiveDirectoryEndpoint, TestTenantID, nil) if err != nil { t.Fatalf("autorest/adal: Unexpected error while creating oauth configuration for tenant: %v.", err) } - expected := "https://login.test.com/tenant-id-test/oauth2/authorize" + expected := fmt.Sprintf("https://login.test.com/%s/oauth2/authorize", TestTenantID) if config.AuthorizeEndpoint.String() != expected { t.Fatalf("autorest/adal: Incorrect authorize url for Tenant from Environment. expected(%s). actual(%v).", expected, config.AuthorizeEndpoint) } - expected = "https://login.test.com/tenant-id-test/oauth2/token" + expected = fmt.Sprintf("https://login.test.com/%s/oauth2/token", TestTenantID) if config.TokenEndpoint.String() != expected { t.Fatalf("autorest/adal: Incorrect authorize url for Tenant from Environment. expected(%s). actual(%v).", expected, config.TokenEndpoint) } - expected = "https://login.test.com/tenant-id-test/oauth2/devicecode" + expected = fmt.Sprintf("https://login.test.com/%s/oauth2/devicecode", TestTenantID) if config.DeviceCodeEndpoint.String() != expected { t.Fatalf("autorest/adal Incorrect devicecode url for Tenant from Environment. expected(%s). actual(%v).", expected, config.DeviceCodeEndpoint) } } func TestNewOAuthConfigWithAPIVersionNotNil(t *testing.T) { - const testActiveDirectoryEndpoint = "https://login.test.com" - const testTenantID = "tenant-id-test" apiVersion := "2.0" - config, err := NewOAuthConfigWithAPIVersion(testActiveDirectoryEndpoint, testTenantID, &apiVersion) + config, err := NewOAuthConfigWithAPIVersion(TestActiveDirectoryEndpoint, TestTenantID, &apiVersion) if err != nil { t.Fatalf("autorest/adal: Unexpected error while creating oauth configuration for tenant: %v.", err) } - expected := "https://login.test.com/tenant-id-test/oauth2/authorize?api-version=2.0" + expected := fmt.Sprintf("https://login.test.com/%s/oauth2/authorize?api-version=2.0", TestTenantID) if config.AuthorizeEndpoint.String() != expected { t.Fatalf("autorest/adal: Incorrect authorize url for Tenant from Environment. expected(%s). actual(%v).", expected, config.AuthorizeEndpoint) } - expected = "https://login.test.com/tenant-id-test/oauth2/token?api-version=2.0" + expected = fmt.Sprintf("https://login.test.com/%s/oauth2/token?api-version=2.0", TestTenantID) if config.TokenEndpoint.String() != expected { t.Fatalf("autorest/adal: Incorrect authorize url for Tenant from Environment. expected(%s). actual(%v).", expected, config.TokenEndpoint) } - expected = "https://login.test.com/tenant-id-test/oauth2/devicecode?api-version=2.0" + expected = fmt.Sprintf("https://login.test.com/%s/oauth2/devicecode?api-version=2.0", TestTenantID) if config.DeviceCodeEndpoint.String() != expected { t.Fatalf("autorest/adal Incorrect devicecode url for Tenant from Environment. expected(%s). actual(%v).", expected, config.DeviceCodeEndpoint) } } + +func TestNewMultiTenantOAuthConfig(t *testing.T) { + cfg, err := NewMultiTenantOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID, TestAuxTenantIDs, Options{}) + if err != nil { + t.Fatalf("autorest/adal: unexpected error while creating multitenant config: %v", err) + } + expected := fmt.Sprintf("https://login.test.com/%s/oauth2/authorize?api-version=1.0", TestTenantID) + if ep := cfg.PrimaryTenant().AuthorizeEndpoint.String(); ep != expected { + t.Fatalf("autorest/adal: Incorrect authorize url for Tenant from Environment. expected(%s). actual(%v).", expected, ep) + } + aux := cfg.AuxiliaryTenants() + if len(aux) == 0 { + t.Fatal("autorest/adal: unexpected zero-length auxiliary tenants") + } + for i := range aux { + expected := fmt.Sprintf("https://login.test.com/aux-tenant-test-%d/oauth2/authorize?api-version=1.0", i) + if ep := aux[i].AuthorizeEndpoint.String(); ep != expected { + t.Fatalf("autorest/adal: Incorrect authorize url for Tenant from Environment. expected(%s). actual(%v).", expected, ep) + } + } +} + +func TestNewMultiTenantOAuthConfigFail(t *testing.T) { + _, err := NewMultiTenantOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID, nil, Options{}) + if err == nil { + t.Fatal("autorest/adal: expected non-nil error") + } + _, err = NewMultiTenantOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID, []string{"one", "two", "three", "four"}, Options{}) + if err == nil { + t.Fatal("autorest/adal: expected non-nil error") + } +} diff --git a/autorest/adal/token.go b/autorest/adal/token.go index effa87ab2..4083e76e6 100644 --- a/autorest/adal/token.go +++ b/autorest/adal/token.go @@ -71,6 +71,12 @@ type OAuthTokenProvider interface { OAuthToken() string } +// MultitenantOAuthTokenProvider provides tokens used for multi-tenant authorization. +type MultitenantOAuthTokenProvider interface { + PrimaryOAuthToken() string + AuxiliaryOAuthTokens() []string +} + // TokenRefreshError is an interface used by errors returned during token refresh. type TokenRefreshError interface { error @@ -983,3 +989,67 @@ func (spt *ServicePrincipalToken) Token() Token { defer spt.refreshLock.RUnlock() return spt.inner.Token } + +// MultiTenantServicePrincipalToken contains tokens for multi-tenant authorization. +type MultiTenantServicePrincipalToken struct { + PrimaryToken *ServicePrincipalToken + AuxiliaryTokens []*ServicePrincipalToken +} + +// PrimaryOAuthToken returns the primary authorization token. +func (mt *MultiTenantServicePrincipalToken) PrimaryOAuthToken() string { + return mt.PrimaryToken.OAuthToken() +} + +// AuxiliaryOAuthTokens returns one to three auxiliary authorization tokens. +func (mt *MultiTenantServicePrincipalToken) AuxiliaryOAuthTokens() []string { + tokens := make([]string, len(mt.AuxiliaryTokens)) + for i := range mt.AuxiliaryTokens { + tokens[i] = mt.AuxiliaryTokens[i].OAuthToken() + } + return tokens +} + +// EnsureFreshWithContext will refresh the token if it will expire within the refresh window (as set by +// RefreshWithin) and autoRefresh flag is on. This method is safe for concurrent use. +func (mt *MultiTenantServicePrincipalToken) EnsureFreshWithContext(ctx context.Context) error { + if err := mt.PrimaryToken.EnsureFreshWithContext(ctx); err != nil { + return fmt.Errorf("failed to refresh primary token: %v", err) + } + for _, aux := range mt.AuxiliaryTokens { + if err := aux.EnsureFreshWithContext(ctx); err != nil { + return fmt.Errorf("failed to refresh auxiliary token: %v", err) + } + } + return nil +} + +// NewMultiTenantServicePrincipalToken creates a new MultiTenantServicePrincipalToken with the specified credentials and resource. +func NewMultiTenantServicePrincipalToken(multiTenantCfg MultiTenantOAuthConfig, clientID string, secret string, resource string) (*MultiTenantServicePrincipalToken, error) { + if err := validateStringParam(clientID, "clientID"); err != nil { + return nil, err + } + if err := validateStringParam(secret, "secret"); err != nil { + return nil, err + } + if err := validateStringParam(resource, "resource"); err != nil { + return nil, err + } + auxTenants := multiTenantCfg.AuxiliaryTenants() + m := MultiTenantServicePrincipalToken{ + AuxiliaryTokens: make([]*ServicePrincipalToken, len(auxTenants)), + } + primary, err := NewServicePrincipalToken(*multiTenantCfg.PrimaryTenant(), clientID, secret, resource) + if err != nil { + return nil, fmt.Errorf("failed to create SPT for primary tenant: %v", err) + } + m.PrimaryToken = primary + for i := range auxTenants { + aux, err := NewServicePrincipalToken(*auxTenants[i], clientID, secret, resource) + if err != nil { + return nil, fmt.Errorf("failed to create SPT for auxiliary tenant: %v", err) + } + m.AuxiliaryTokens[i] = aux + } + return &m, nil +} diff --git a/autorest/adal/token_test.go b/autorest/adal/token_test.go index 9f4f1fa43..601e96430 100644 --- a/autorest/adal/token_test.go +++ b/autorest/adal/token_test.go @@ -845,6 +845,22 @@ func TestMarshalInnerToken(t *testing.T) { } } +func TestNewMultiTenantServicePrincipalToken(t *testing.T) { + cfg, err := NewMultiTenantOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID, TestAuxTenantIDs, Options{}) + if err != nil { + t.Fatalf("autorest/adal: unexpected error while creating multitenant config: %v", err) + } + mt, err := NewMultiTenantServicePrincipalToken(cfg, "clientID", "superSecret", "resource") + if !strings.Contains(mt.PrimaryToken.inner.OauthConfig.AuthorizeEndpoint.String(), TestTenantID) { + t.Fatal("didn't find primary tenant ID in primary SPT") + } + for i := range mt.AuxiliaryTokens { + if ep := mt.AuxiliaryTokens[i].inner.OauthConfig.AuthorizeEndpoint.String(); !strings.Contains(ep, fmt.Sprintf("%s%d", TestAuxTenantPrefix, i)) { + t.Fatalf("didn't find auxiliary tenant ID in token %s", ep) + } + } +} + func newTokenJSON(expiresOn string, resource string) string { return fmt.Sprintf(`{ "access_token" : "accessToken", diff --git a/autorest/authorization.go b/autorest/authorization.go index 2e24b4b39..380865cd6 100644 --- a/autorest/authorization.go +++ b/autorest/authorization.go @@ -285,3 +285,52 @@ func (ba *BasicAuthorizer) WithAuthorization() PrepareDecorator { return NewAPIKeyAuthorizerWithHeaders(headers).WithAuthorization() } + +// MultiTenantServicePrincipalTokenAuthorizer provides authentication across tenants. +type MultiTenantServicePrincipalTokenAuthorizer interface { + WithAuthorization() PrepareDecorator +} + +// NewMultiTenantServicePrincipalTokenAuthorizer crates a BearerAuthorizer using the given token provider +func NewMultiTenantServicePrincipalTokenAuthorizer(tp adal.MultitenantOAuthTokenProvider) MultiTenantServicePrincipalTokenAuthorizer { + return &multiTenantSPTAuthorizer{tp: tp} +} + +type multiTenantSPTAuthorizer struct { + tp adal.MultitenantOAuthTokenProvider +} + +// WithAuthorization returns a PrepareDecorator that adds an HTTP Authorization header using the +// primary token along with the auxiliary authorization header using the auxiliary tokens. +// +// By default, the token will be automatically refreshed through the Refresher interface. +func (mt multiTenantSPTAuthorizer) 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 refresher, ok := mt.tp.(adal.RefresherWithContext); ok { + err = refresher.EnsureFreshWithContext(r.Context()) + if err != nil { + var resp *http.Response + if tokError, ok := err.(adal.TokenRefreshError); ok { + resp = tokError.Response() + } + return r, NewErrorWithError(err, "azure.multiTenantSPTAuthorizer", "WithAuthorization", resp, + "Failed to refresh one or more Tokens for request to %s", r.URL) + } + } + r, err = Prepare(r, WithHeader(headerAuthorization, fmt.Sprintf("Bearer %s", mt.tp.PrimaryOAuthToken()))) + if err != nil { + return r, err + } + auxTokens := mt.tp.AuxiliaryOAuthTokens() + for i := range auxTokens { + auxTokens[i] = fmt.Sprintf("Bearer %s", auxTokens[i]) + } + return Prepare(r, WithHeader(headerAuxAuthorization, strings.Join(auxTokens, "; "))) + }) + } +} diff --git a/autorest/authorization_test.go b/autorest/authorization_test.go index 85d8b0fe7..1c70409ad 100644 --- a/autorest/authorization_test.go +++ b/autorest/authorization_test.go @@ -250,3 +250,52 @@ func TestBasicAuthorizationPasswordOnly(t *testing.T) { t.Fatalf("BasicAuthorizer#WithAuthorization failed to set %s header", authorization) } } + +type mockMTSPTProvider struct { + p string + a []string +} + +func (m mockMTSPTProvider) PrimaryOAuthToken() string { + return m.p +} + +func (m mockMTSPTProvider) AuxiliaryOAuthTokens() []string { + return m.a +} + +func TestMultitenantAuthorizationOne(t *testing.T) { + mtSPTProvider := mockMTSPTProvider{ + p: "primary", + a: []string{"aux1"}, + } + mt := NewMultiTenantServicePrincipalTokenAuthorizer(mtSPTProvider) + req, err := Prepare(mocks.NewRequest(), mt.WithAuthorization()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if primary := req.Header.Get(headerAuthorization); primary != "Bearer primary" { + t.Fatalf("bad primary authorization header %s", primary) + } + if aux := req.Header.Get(headerAuxAuthorization); aux != "Bearer aux1" { + t.Fatalf("bad auxiliary authorization header %s", aux) + } +} + +func TestMultitenantAuthorizationThree(t *testing.T) { + mtSPTProvider := mockMTSPTProvider{ + p: "primary", + a: []string{"aux1", "aux2", "aux3"}, + } + mt := NewMultiTenantServicePrincipalTokenAuthorizer(mtSPTProvider) + req, err := Prepare(mocks.NewRequest(), mt.WithAuthorization()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if primary := req.Header.Get(headerAuthorization); primary != "Bearer primary" { + t.Fatalf("bad primary authorization header %s", primary) + } + if aux := req.Header.Get(headerAuxAuthorization); aux != "Bearer aux1; Bearer aux2; Bearer aux3" { + t.Fatalf("bad auxiliary authorization header %s", aux) + } +} diff --git a/autorest/azure/auth/auth.go b/autorest/azure/auth/auth.go index 20855d4ab..d852f77a8 100644 --- a/autorest/azure/auth/auth.go +++ b/autorest/azure/auth/auth.go @@ -40,6 +40,7 @@ import ( const ( SubscriptionID = "AZURE_SUBSCRIPTION_ID" TenantID = "AZURE_TENANT_ID" + AuxiliaryTenantIDs = "AZURE_AUXILIARY_TENANT_IDS" ClientID = "AZURE_CLIENT_ID" ClientSecret = "AZURE_CLIENT_SECRET" CertificatePath = "AZURE_CERTIFICATE_PATH" @@ -96,6 +97,7 @@ func GetSettingsFromEnvironment() (s EnvironmentSettings, err error) { } s.setValue(SubscriptionID) s.setValue(TenantID) + s.setValue(AuxiliaryTenantIDs) s.setValue(ClientID) s.setValue(ClientSecret) s.setValue(CertificatePath) @@ -145,6 +147,12 @@ func (settings EnvironmentSettings) GetClientCredentials() (ClientCredentialsCon config := NewClientCredentialsConfig(clientID, secret, tenantID) config.AADEndpoint = settings.Environment.ActiveDirectoryEndpoint config.Resource = settings.Values[Resource] + if auxTenants, ok := settings.Values[AuxiliaryTenantIDs]; ok { + config.AuxTenants = strings.Split(auxTenants, ";") + for i := range config.AuxTenants { + config.AuxTenants[i] = strings.TrimSpace(config.AuxTenants[i]) + } + } return config, nil } @@ -546,6 +554,7 @@ type ClientCredentialsConfig struct { ClientID string ClientSecret string TenantID string + AuxTenants []string AADEndpoint string Resource string } @@ -559,13 +568,29 @@ func (ccc ClientCredentialsConfig) ServicePrincipalToken() (*adal.ServicePrincip return adal.NewServicePrincipalToken(*oauthConfig, ccc.ClientID, ccc.ClientSecret, ccc.Resource) } +// MultiTenantServicePrincipalToken creates a MultiTenantServicePrincipalToken from client credentials. +func (ccc ClientCredentialsConfig) MultiTenantServicePrincipalToken() (*adal.MultiTenantServicePrincipalToken, error) { + oauthConfig, err := adal.NewMultiTenantOAuthConfig(ccc.AADEndpoint, ccc.TenantID, ccc.AuxTenants, adal.Options{}) + if err != nil { + return nil, err + } + return adal.NewMultiTenantServicePrincipalToken(oauthConfig, ccc.ClientID, ccc.ClientSecret, ccc.Resource) +} + // Authorizer gets the authorizer from client credentials. func (ccc ClientCredentialsConfig) Authorizer() (autorest.Authorizer, error) { - spToken, err := ccc.ServicePrincipalToken() + if len(ccc.AuxTenants) == 0 { + spToken, err := ccc.ServicePrincipalToken() + if err != nil { + return nil, fmt.Errorf("failed to get SPT from client credentials: %v", err) + } + return autorest.NewBearerAuthorizer(spToken), nil + } + mtSPT, err := ccc.MultiTenantServicePrincipalToken() if err != nil { - return nil, fmt.Errorf("failed to get oauth token from client credentials: %v", err) + return nil, fmt.Errorf("failed to get multitenant SPT from client credentials: %v", err) } - return autorest.NewBearerAuthorizer(spToken), nil + return autorest.NewMultiTenantServicePrincipalTokenAuthorizer(mtSPT), nil } // ClientCertificateConfig provides the options to get a bearer authorizer from a client certificate. diff --git a/autorest/azure/auth/auth_test.go b/autorest/azure/auth/auth_test.go index 7c8eb3d29..6c730ff96 100644 --- a/autorest/azure/auth/auth_test.go +++ b/autorest/azure/auth/auth_test.go @@ -20,6 +20,7 @@ import ( "reflect" "testing" + "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/azure" ) @@ -268,3 +269,36 @@ func TestFileClientCertificateAuthorizer(t *testing.T) { t.Fail() } } + +func TestMultitenantClientCredentials(t *testing.T) { + setDefaultEnv() + os.Setenv(AuxiliaryTenantIDs, "aux-tenant-1;aux-tenant-2;aux-tenant3") + defer func() { + os.Setenv(AuxiliaryTenantIDs, "") + }() + settings, err := GetSettingsFromEnvironment() + if err != nil { + t.Fatalf("failed to get settings from environment: %v", err) + } + if settings.Values[AuxiliaryTenantIDs] == "" { + t.Fatal("auxiliary tenant IDs are missing in settings") + } + ccc, err := settings.GetClientCredentials() + if err != nil { + t.Fatalf("failed to get client credentials config: %v", err) + } + if len(ccc.AuxTenants) == 0 { + t.Fatal("auxiliary tenant IDs are missing in config") + } + expected := []string{"aux-tenant-1", "aux-tenant-2", "aux-tenant3"} + if !reflect.DeepEqual(ccc.AuxTenants, expected) { + t.Fatalf("expected auxiliary tenants '%s', got '%s'", expected, ccc.AuxTenants) + } + a, err := ccc.Authorizer() + if err != nil { + t.Fatalf("failed to create authorizer: %v", err) + } + if _, ok := a.(autorest.MultiTenantServicePrincipalTokenAuthorizer); !ok { + t.Fatal("authorizer doesn't implement MultiTenantServicePrincipalTokenAuthorizer") + } +} diff --git a/autorest/preparer.go b/autorest/preparer.go index 6e955a4ba..489140224 100644 --- a/autorest/preparer.go +++ b/autorest/preparer.go @@ -32,9 +32,10 @@ const ( mimeTypeOctetStream = "application/octet-stream" mimeTypeFormPost = "application/x-www-form-urlencoded" - headerAuthorization = "Authorization" - headerContentType = "Content-Type" - headerUserAgent = "User-Agent" + headerAuthorization = "Authorization" + headerAuxAuthorization = "x-ms-authorization-auxiliary" + headerContentType = "Content-Type" + headerUserAgent = "User-Agent" ) // Preparer is the interface that wraps the Prepare method. From 31ab60d899bb4dd8f70ce492b1d6522499b53b66 Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Mon, 1 Jul 2019 12:48:18 -0700 Subject: [PATCH 13/29] rename Options to OAuthOptions (#415) --- autorest/adal/config.go | 8 ++++---- autorest/adal/config_test.go | 6 +++--- autorest/adal/token_test.go | 2 +- autorest/azure/auth/auth.go | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/autorest/adal/config.go b/autorest/adal/config.go index b486b648e..fa5964742 100644 --- a/autorest/adal/config.go +++ b/autorest/adal/config.go @@ -100,12 +100,12 @@ type MultiTenantOAuthConfig interface { AuxiliaryTenants() []*OAuthConfig } -// Options contains optional OAuthConfig creation arguments. -type Options struct { +// OAuthOptions contains optional OAuthConfig creation arguments. +type OAuthOptions struct { APIVersion string } -func (c Options) apiVersion() string { +func (c OAuthOptions) apiVersion() string { if c.APIVersion != "" { return fmt.Sprintf("?api-version=%s", c.APIVersion) } @@ -114,7 +114,7 @@ func (c Options) apiVersion() string { // NewMultiTenantOAuthConfig creates an object that support multitenant OAuth configuration. // See https://docs.microsoft.com/en-us/azure/azure-resource-manager/authenticate-multi-tenant for more information. -func NewMultiTenantOAuthConfig(activeDirectoryEndpoint, primaryTenantID string, auxiliaryTenantIDs []string, options Options) (MultiTenantOAuthConfig, error) { +func NewMultiTenantOAuthConfig(activeDirectoryEndpoint, primaryTenantID string, auxiliaryTenantIDs []string, options OAuthOptions) (MultiTenantOAuthConfig, error) { if len(auxiliaryTenantIDs) == 0 || len(auxiliaryTenantIDs) > 3 { return nil, errors.New("must specify one to three auxiliary tenants") } diff --git a/autorest/adal/config_test.go b/autorest/adal/config_test.go index 10584ca9c..f6c11482d 100644 --- a/autorest/adal/config_test.go +++ b/autorest/adal/config_test.go @@ -94,7 +94,7 @@ func TestNewOAuthConfigWithAPIVersionNotNil(t *testing.T) { } func TestNewMultiTenantOAuthConfig(t *testing.T) { - cfg, err := NewMultiTenantOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID, TestAuxTenantIDs, Options{}) + cfg, err := NewMultiTenantOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID, TestAuxTenantIDs, OAuthOptions{}) if err != nil { t.Fatalf("autorest/adal: unexpected error while creating multitenant config: %v", err) } @@ -115,11 +115,11 @@ func TestNewMultiTenantOAuthConfig(t *testing.T) { } func TestNewMultiTenantOAuthConfigFail(t *testing.T) { - _, err := NewMultiTenantOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID, nil, Options{}) + _, err := NewMultiTenantOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID, nil, OAuthOptions{}) if err == nil { t.Fatal("autorest/adal: expected non-nil error") } - _, err = NewMultiTenantOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID, []string{"one", "two", "three", "four"}, Options{}) + _, err = NewMultiTenantOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID, []string{"one", "two", "three", "four"}, OAuthOptions{}) if err == nil { t.Fatal("autorest/adal: expected non-nil error") } diff --git a/autorest/adal/token_test.go b/autorest/adal/token_test.go index 601e96430..a0dc6d748 100644 --- a/autorest/adal/token_test.go +++ b/autorest/adal/token_test.go @@ -846,7 +846,7 @@ func TestMarshalInnerToken(t *testing.T) { } func TestNewMultiTenantServicePrincipalToken(t *testing.T) { - cfg, err := NewMultiTenantOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID, TestAuxTenantIDs, Options{}) + cfg, err := NewMultiTenantOAuthConfig(TestActiveDirectoryEndpoint, TestTenantID, TestAuxTenantIDs, OAuthOptions{}) if err != nil { t.Fatalf("autorest/adal: unexpected error while creating multitenant config: %v", err) } diff --git a/autorest/azure/auth/auth.go b/autorest/azure/auth/auth.go index d852f77a8..b6ef12839 100644 --- a/autorest/azure/auth/auth.go +++ b/autorest/azure/auth/auth.go @@ -570,7 +570,7 @@ func (ccc ClientCredentialsConfig) ServicePrincipalToken() (*adal.ServicePrincip // MultiTenantServicePrincipalToken creates a MultiTenantServicePrincipalToken from client credentials. func (ccc ClientCredentialsConfig) MultiTenantServicePrincipalToken() (*adal.MultiTenantServicePrincipalToken, error) { - oauthConfig, err := adal.NewMultiTenantOAuthConfig(ccc.AADEndpoint, ccc.TenantID, ccc.AuxTenants, adal.Options{}) + oauthConfig, err := adal.NewMultiTenantOAuthConfig(ccc.AADEndpoint, ccc.TenantID, ccc.AuxTenants, adal.OAuthOptions{}) if err != nil { return nil, err } From a0512ab9231622a239aaab1da9f80a6b1fe8b39c Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Mon, 8 Jul 2019 09:54:44 -0700 Subject: [PATCH 14/29] 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 --- autorest/sender.go | 112 ++++++++++++++++++++++++++++------------ autorest/sender_test.go | 32 ++++++++++++ 2 files changed, 112 insertions(+), 32 deletions(-) diff --git a/autorest/sender.go b/autorest/sender.go index 2f4b0ddda..b918833fa 100644 --- a/autorest/sender.go +++ b/autorest/sender.go @@ -15,6 +15,7 @@ package autorest // limitations under the License. import ( + "context" "fmt" "log" "math" @@ -25,6 +26,23 @@ import ( "github.com/Azure/go-autorest/tracing" ) +// used as a key type in context.WithValue() +type ctxSendDecorators struct{} + +// WithSendDecorators adds the specified SendDecorators to the provided context. +func WithSendDecorators(ctx context.Context, sendDecorator []SendDecorator) context.Context { + return context.WithValue(ctx, ctxSendDecorators{}, sendDecorator) +} + +// GetSendDecorators returns the SendDecorators in the provided context or the provided default SendDecorators. +func GetSendDecorators(ctx context.Context, defaultSendDecorators ...SendDecorator) []SendDecorator { + inCtx := ctx.Value(ctxSendDecorators{}) + if sd, ok := inCtx.([]SendDecorator); ok { + return sd + } + return defaultSendDecorators +} + // Sender is the interface that wraps the Do method to send HTTP requests. // // The standard http.Client conforms to this interface. @@ -211,43 +229,59 @@ func DoRetryForAttempts(attempts int, backoff time.Duration) SendDecorator { // DoRetryForStatusCodes returns a SendDecorator that retries for specified statusCodes for up to the specified // number of attempts, exponentially backing off between requests using the supplied backoff -// time.Duration (which may be zero). Retrying may be canceled by closing the optional channel on -// the http.Request. +// time.Duration (which may be zero). Retrying may be canceled by cancelling the context on the http.Request. +// NOTE: Code http.StatusTooManyRequests (429) will *not* be counted against the number of attempts. func DoRetryForStatusCodes(attempts int, backoff time.Duration, codes ...int) SendDecorator { return func(s Sender) Sender { - return SenderFunc(func(r *http.Request) (resp *http.Response, err error) { - rr := NewRetriableRequest(r) - // Increment to add the first call (attempts denotes number of retries) - for attempt := 0; attempt < attempts+1; { - err = rr.Prepare() - if err != nil { - return resp, err - } - resp, err = s.Do(rr.Request()) - // if the error isn't temporary don't bother retrying - if err != nil && !IsTemporaryNetworkError(err) { - return nil, err - } - // we want to retry if err is not nil (e.g. transient network failure). note that for failed authentication - // resp and err will both have a value, so in this case we don't want to retry as it will never succeed. - if err == nil && !ResponseHasStatusCode(resp, codes...) || IsTokenRefreshError(err) { - return resp, err - } - delayed := DelayWithRetryAfter(resp, r.Context().Done()) - if !delayed && !DelayForBackoff(backoff, attempt, r.Context().Done()) { - return resp, r.Context().Err() - } - // don't count a 429 against the number of attempts - // so that we continue to retry until it succeeds - if resp == nil || resp.StatusCode != http.StatusTooManyRequests { - attempt++ - } - } - return resp, err + return SenderFunc(func(r *http.Request) (*http.Response, error) { + return doRetryForStatusCodesImpl(s, r, false, attempts, backoff, 0, codes...) }) } } +// DoRetryForStatusCodesWithCap returns a SendDecorator that retries for specified statusCodes for up to the +// specified number of attempts, exponentially backing off between requests using the supplied backoff +// time.Duration (which may be zero). To cap the maximum possible delay between iterations specify a value greater +// than zero for cap. Retrying may be canceled by cancelling the context on the http.Request. +func DoRetryForStatusCodesWithCap(attempts int, backoff, cap time.Duration, codes ...int) SendDecorator { + return func(s Sender) Sender { + return SenderFunc(func(r *http.Request) (*http.Response, error) { + return doRetryForStatusCodesImpl(s, r, true, attempts, backoff, cap, codes...) + }) + } +} + +func doRetryForStatusCodesImpl(s Sender, r *http.Request, count429 bool, attempts int, backoff, cap time.Duration, codes ...int) (resp *http.Response, err error) { + rr := NewRetriableRequest(r) + // Increment to add the first call (attempts denotes number of retries) + for attempt := 0; attempt < attempts+1; { + err = rr.Prepare() + if err != nil { + return + } + resp, err = s.Do(rr.Request()) + // if the error isn't temporary don't bother retrying + if err != nil && !IsTemporaryNetworkError(err) { + return + } + // we want to retry if err is not nil (e.g. transient network failure). note that for failed authentication + // resp and err will both have a value, so in this case we don't want to retry as it will never succeed. + if err == nil && !ResponseHasStatusCode(resp, codes...) || IsTokenRefreshError(err) { + return resp, err + } + delayed := DelayWithRetryAfter(resp, r.Context().Done()) + if !delayed && !DelayForBackoffWithCap(backoff, cap, attempt, r.Context().Done()) { + return resp, r.Context().Err() + } + // when count429 == false don't count a 429 against the number + // of attempts so that we continue to retry until it succeeds + if count429 || (resp == nil || resp.StatusCode != http.StatusTooManyRequests) { + attempt++ + } + } + return resp, err +} + // DelayWithRetryAfter invokes time.After for the duration specified in the "Retry-After" header. // The value of Retry-After can be either the number of seconds or a date in RFC1123 format. // The function returns true after successfully waiting for the specified duration. If there is @@ -325,8 +359,22 @@ func WithLogging(logger *log.Logger) SendDecorator { // Note: Passing attempt 1 will result in doubling "backoff" duration. Treat this as a zero-based attempt // count. func DelayForBackoff(backoff time.Duration, attempt int, cancel <-chan struct{}) bool { + return DelayForBackoffWithCap(backoff, 0, attempt, cancel) +} + +// DelayForBackoffWithCap invokes time.After for the supplied backoff duration raised to the power of +// passed attempt (i.e., an exponential backoff delay). Backoff duration is in seconds and can set +// to zero for no delay. To cap the maximum possible delay specify a value greater than zero for cap. +// The delay may be canceled by closing the passed channel. If terminated early, returns false. +// Note: Passing attempt 1 will result in doubling "backoff" duration. Treat this as a zero-based attempt +// count. +func DelayForBackoffWithCap(backoff, cap time.Duration, attempt int, cancel <-chan struct{}) bool { + d := time.Duration(backoff.Seconds()*math.Pow(2, float64(attempt))) * time.Second + if cap > 0 && d > cap { + d = cap + } select { - case <-time.After(time.Duration(backoff.Seconds()*math.Pow(2, float64(attempt))) * time.Second): + case <-time.After(d): return true case <-cancel: return false diff --git a/autorest/sender_test.go b/autorest/sender_test.go index 29bc7e91a..ab92566d4 100644 --- a/autorest/sender_test.go +++ b/autorest/sender_test.go @@ -513,6 +513,15 @@ func TestDelayForBackoff(t *testing.T) { } } +func TestDelayForBackoffWithCap(t *testing.T) { + d := 2 * time.Second + start := time.Now() + DelayForBackoffWithCap(d, 1*time.Second, 0, nil) + if time.Since(start) >= d { + t.Fatal("autorest: DelayForBackoffWithCap delayed for too long") + } +} + func TestDelayForBackoff_Cancels(t *testing.T) { cancel := make(chan struct{}) delay := 5 * time.Second @@ -979,3 +988,26 @@ func TestDoRetryForStatusCodes_Race(t *testing.T) { } wg.Wait() } + +func TestGetSendDecorators(t *testing.T) { + sd := GetSendDecorators(context.Background()) + if l := len(sd); l != 0 { + t.Fatalf("expected zero length but got %d", l) + } + sd = GetSendDecorators(context.Background(), DoCloseIfError(), DoErrorIfStatusCode()) + if l := len(sd); l != 2 { + t.Fatalf("expected length of two but got %d", l) + } +} + +func TestWithSendDecorators(t *testing.T) { + ctx := WithSendDecorators(context.Background(), []SendDecorator{DoRetryForAttempts(5, 5*time.Second)}) + sd := GetSendDecorators(ctx) + if l := len(sd); l != 1 { + t.Fatalf("expected length of one but got %d", l) + } + sd = GetSendDecorators(ctx, DoCloseIfError(), DoErrorIfStatusCode()) + if l := len(sd); l != 1 { + t.Fatalf("expected length of one but got %d", l) + } +} From 1affb820247135fe348d1561726a4531c2a057d7 Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Wed, 10 Jul 2019 16:06:13 -0700 Subject: [PATCH 15/29] 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) --- .travis.yml | 34 ------------------------ CHANGELOG.md | 14 ++++++++++ README.md | 2 +- autorest/adal/persist_test.go | 2 +- autorest/version.go | 2 +- azure-pipelines.yml | 49 +++++++++++++++++++++++++++++++++++ 6 files changed, 66 insertions(+), 37 deletions(-) delete mode 100644 .travis.yml create mode 100644 azure-pipelines.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 18ed6713a..000000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ -sudo: false - -language: go - -go: - - 1.10.x - - 1.11.x - - 1.12.x - -env: - global: - - DEP_RELEASE_TAG=v0.5.1 # so the script knows which version to use - - GOSEC_RELEASE_TAG=1.3.0 - -before_install: - - curl -sSL https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - - curl -sfL https://raw.githubusercontent.com/securego/gosec/master/install.sh | sh -s -- -b $GOPATH/bin $GOSEC_RELEASE_TAG - -install: - - dep ensure -v - - go install ./vendor/golang.org/x/lint/golint - -script: - - grep -L -r --include *.go --exclude-dir vendor -P "Copyright (\d{4}|\(c\)) Microsoft" ./ | tee /dev/stderr | test -z "$(< /dev/stdin)" - - if [[ $TRAVIS_GO_VERSION == 1.11* ]]; then test -z "$(gofmt -s -l -w ./autorest/. | tee /dev/stderr)"; fi - - test -z "$(golint ./autorest/... | tee /dev/stderr)" - - go vet ./autorest/... - #- test -z "$(gosec ./autorest/... | tee /dev/stderr | grep Error)" - - go build -v ./autorest/... - - go test -race -v ./autorest/... - -cache: - directories: - - $GOPATH/pkg/dep diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b753704c..d5748316e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # CHANGELOG +## v12.3.0 + +### New Features + +- 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. + See `adal.NewMultiTenantOAuthConfig`, `adal.NewMultiTenantServicePrincipalToken` and `autorest.NewMultiTenantServicePrincipalTokenAuthorizer` + along with their supporting types and methods. +- 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. + ## v12.2.0 ### New Features diff --git a/README.md b/README.md index 9a7b13a35..d78ddda69 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # go-autorest [![GoDoc](https://godoc.org/github.com/Azure/go-autorest/autorest?status.png)](https://godoc.org/github.com/Azure/go-autorest/autorest) -[![Build Status](https://travis-ci.org/Azure/go-autorest.svg?branch=master)](https://travis-ci.org/Azure/go-autorest) +[![Build Status](https://dev.azure.com/azure-sdk/public/_apis/build/status/go/Azure.go-autorest?branchName=master)](https://dev.azure.com/azure-sdk/public/_build/latest?definitionId=625&branchName=master) [![Go Report Card](https://goreportcard.com/badge/Azure/go-autorest)](https://goreportcard.com/report/Azure/go-autorest) Package go-autorest provides an HTTP request client for use with [Autorest](https://github.com/Azure/autorest.go)-generated API client packages. diff --git a/autorest/adal/persist_test.go b/autorest/adal/persist_test.go index baa6f00bf..2c98c127e 100644 --- a/autorest/adal/persist_test.go +++ b/autorest/adal/persist_test.go @@ -159,7 +159,7 @@ func TestSaveTokenFailsNoPermission(t *testing.T) { } func TestSaveTokenFailsCantCreate(t *testing.T) { - tokenPath := "/thiswontwork" + tokenPath := "/usr/thiswontwork" if runtime.GOOS == "windows" { tokenPath = path.Join(os.Getenv("windir"), "system32") } diff --git a/autorest/version.go b/autorest/version.go index 8ba0f591d..ec7e84817 100644 --- a/autorest/version.go +++ b/autorest/version.go @@ -19,7 +19,7 @@ import ( "runtime" ) -const number = "v12.2.0" +const number = "v12.3.0" var ( userAgent = fmt.Sprintf("Go/%s (%s-%s) go-autorest/%s", diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 000000000..df553150b --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,49 @@ +pool: + vmImage: 'Ubuntu 16.04' + +variables: + GOROOT: '/usr/local/go1.12' + GOPATH: '$(system.defaultWorkingDirectory)/work' + sdkPath: '$(GOPATH)/src/github.com/$(build.repository.name)' + +steps: +- script: | + set -e + mkdir -p '$(GOPATH)/bin' + mkdir -p '$(sdkPath)' + shopt -s extglob + mv !(work) '$(sdkPath)' + echo '##vso[task.prependpath]$(GOROOT)/bin' + echo '##vso[task.prependpath]$(GOPATH)/bin' + displayName: 'Create Go Workspace' +- script: | + set -e + curl -sSL https://raw.githubusercontent.com/golang/dep/master/install.sh | sh + dep ensure -v + go install ./vendor/golang.org/x/lint/golint + workingDirectory: '$(sdkPath)' + displayName: 'Install Dependencies' +- script: go vet ./autorest/... + workingDirectory: '$(sdkPath)' + displayName: 'Vet' +- script: go build -v ./autorest/... + workingDirectory: '$(sdkPath)' + displayName: 'Build' +- script: go test -race -v ./autorest/... + workingDirectory: '$(sdkPath)' + displayName: 'Run Tests' +- script: grep -L -r --include *.go --exclude-dir vendor -P "Copyright (\d{4}|\(c\)) Microsoft" ./ | tee >&2 + workingDirectory: '$(sdkPath)' + displayName: 'Copyright Header Check' + failOnStderr: true + condition: succeededOrFailed() +- script: gofmt -s -l -w ./autorest/. >&2 + workingDirectory: '$(sdkPath)' + displayName: 'Format Check' + failOnStderr: true + condition: succeededOrFailed() +- script: golint ./autorest/... >&2 + workingDirectory: '$(sdkPath)' + displayName: 'Linter Check' + failOnStderr: true + condition: succeededOrFailed() From 6d8e7e7411d900e3d472c436d74eb021658578e6 Mon Sep 17 00:00:00 2001 From: Patrick Decat Date: Mon, 7 Oct 2019 19:00:07 +0200 Subject: [PATCH 16/29] Use accessTokens.json from AZURE_CONFIG_DIR if AZURE_ACCESS_TOKEN_FILE is not set before falling back on ~/.azure/ (#471) --- autorest/azure/cli/profile.go | 6 +++++- autorest/azure/cli/token.go | 21 +++++++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/autorest/azure/cli/profile.go b/autorest/azure/cli/profile.go index a336b958d..f45c3a516 100644 --- a/autorest/azure/cli/profile.go +++ b/autorest/azure/cli/profile.go @@ -51,9 +51,13 @@ type User struct { const azureProfileJSON = "azureProfile.json" +func configDir() string { + return os.Getenv("AZURE_CONFIG_DIR") +} + // ProfilePath returns the path where the Azure Profile is stored from the Azure CLI func ProfilePath() (string, error) { - if cfgDir := os.Getenv("AZURE_CONFIG_DIR"); cfgDir != "" { + if cfgDir := configDir(); cfgDir != "" { return filepath.Join(cfgDir, azureProfileJSON), nil } return homedir.Expand("~/.azure/" + azureProfileJSON) diff --git a/autorest/azure/cli/token.go b/autorest/azure/cli/token.go index 810075ba6..44ff446f6 100644 --- a/autorest/azure/cli/token.go +++ b/autorest/azure/cli/token.go @@ -20,6 +20,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "regexp" "runtime" "strconv" @@ -44,6 +45,8 @@ type Token struct { UserID string `json:"userId"` } +const accessTokensJSON = "accessTokens.json" + // ToADALToken converts an Azure CLI `Token`` to an `adal.Token`` func (t Token) ToADALToken() (converted adal.Token, err error) { tokenExpirationDate, err := ParseExpirationDate(t.ExpiresOn) @@ -68,17 +71,19 @@ func (t Token) ToADALToken() (converted adal.Token, err error) { // AccessTokensPath returns the path where access tokens are stored from the Azure CLI // TODO(#199): add unit test. func AccessTokensPath() (string, error) { - // Azure-CLI allows user to customize the path of access tokens thorugh environment variable. - var accessTokenPath = os.Getenv("AZURE_ACCESS_TOKEN_FILE") - var err error + // Azure-CLI allows user to customize the path of access tokens through environment variable. + if accessTokenPath := os.Getenv("AZURE_ACCESS_TOKEN_FILE"); accessTokenPath != "" { + return accessTokenPath, nil + } - // Fallback logic to default path on non-cloud-shell environment. - // TODO(#200): remove the dependency on hard-coding path. - if accessTokenPath == "" { - accessTokenPath, err = homedir.Expand("~/.azure/accessTokens.json") + // Azure-CLI allows user to customize the path to Azure config directory through environment variable. + if cfgDir := configDir(); cfgDir != "" { + return filepath.Join(cfgDir, accessTokensJSON), nil } - return accessTokenPath, err + // Fallback logic to default path on non-cloud-shell environment. + // TODO(#200): remove the dependency on hard-coding path. + return homedir.Expand("~/.azure/" + accessTokensJSON) } // ParseExpirationDate parses either a Azure CLI or CloudShell date into a time object From 87a0e4306b453713a5a22e5faba39aea37ef7a75 Mon Sep 17 00:00:00 2001 From: Tom Harvey Date: Tue, 22 Oct 2019 00:20:11 +0200 Subject: [PATCH 17/29] 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 --- autorest/azure/azure.go | 19 ++++++++++++++----- autorest/azure/azure_test.go | 29 +++++++++++++++++++++++++++++ autorest/azure/rp.go | 12 ++++++++---- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/autorest/azure/azure.go b/autorest/azure/azure.go index 3a0a439ff..26be936b7 100644 --- a/autorest/azure/azure.go +++ b/autorest/azure/azure.go @@ -17,6 +17,7 @@ package azure // limitations under the License. import ( + "bytes" "encoding/json" "fmt" "io/ioutil" @@ -143,7 +144,7 @@ type RequestError struct { autorest.DetailedError // The error returned by the Azure service. - ServiceError *ServiceError `json:"error"` + ServiceError *ServiceError `json:"error" xml:"Error"` // The request id (from the x-ms-request-id-header) of the request. RequestID string @@ -285,26 +286,34 @@ func WithErrorUnlessStatusCode(codes ...int) autorest.RespondDecorator { var e RequestError defer resp.Body.Close() + encodedAs := autorest.EncodedAsJSON + if strings.Contains(resp.Header.Get("Content-Type"), "xml") { + encodedAs = autorest.EncodedAsXML + } + // Copy and replace the Body in case it does not contain an error object. // This will leave the Body available to the caller. - b, decodeErr := autorest.CopyAndDecode(autorest.EncodedAsJSON, resp.Body, &e) + b, decodeErr := autorest.CopyAndDecode(encodedAs, resp.Body, &e) resp.Body = ioutil.NopCloser(&b) if decodeErr != nil { return fmt.Errorf("autorest/azure: error response cannot be parsed: %q error: %v", b.String(), decodeErr) } if e.ServiceError == nil { // Check if error is unwrapped ServiceError - if err := json.Unmarshal(b.Bytes(), &e.ServiceError); err != nil { + decoder := autorest.NewDecoder(encodedAs, bytes.NewReader(b.Bytes())) + if err := decoder.Decode(&e.ServiceError); err != nil { return err } } if e.ServiceError.Message == "" { // if we're here it means the returned error wasn't OData v4 compliant. - // try to unmarshal the body as raw JSON in hopes of getting something. + // try to unmarshal the body in hopes of getting something. rawBody := map[string]interface{}{} - if err := json.Unmarshal(b.Bytes(), &rawBody); err != nil { + decoder := autorest.NewDecoder(encodedAs, bytes.NewReader(b.Bytes())) + if err := decoder.Decode(&rawBody); err != nil { return err } + e.ServiceError = &ServiceError{ Code: "Unknown", Message: "Unknown service error", diff --git a/autorest/azure/azure_test.go b/autorest/azure/azure_test.go index a99ccae7f..5438653c0 100644 --- a/autorest/azure/azure_test.go +++ b/autorest/azure/azure_test.go @@ -599,6 +599,35 @@ func TestParseResourceID_WithMalformedResourceID(t *testing.T) { } } +func TestRequestErrorString_WithXMLError(t *testing.T) { + j := ` + + InternalError + Internal service error. + ` + uuid := "71FDB9F4-5E49-4C12-B266-DE7B4FD999A6" + r := mocks.NewResponseWithContent(j) + mocks.SetResponseHeader(r, HeaderRequestID, uuid) + r.Request = mocks.NewRequest() + r.StatusCode = http.StatusInternalServerError + r.Status = http.StatusText(r.StatusCode) + r.Header.Add("Content-Type", "text/xml") + + err := autorest.Respond(r, + WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + + if err == nil { + t.Fatalf("azure: returned nil error for proper error response") + } + azErr, _ := err.(*RequestError) + const expected = `autorest/azure: Service returned an error. Status=500 Code="InternalError" Message="Internal service error."` + if got := azErr.Error(); expected != got { + fmt.Println(got) + t.Fatalf("azure: send wrong RequestError.\nexpected=%v\ngot=%v", expected, got) + } +} + func withErrorPrepareDecorator(e *error) autorest.PrepareDecorator { return func(p autorest.Preparer) autorest.Preparer { return autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) { diff --git a/autorest/azure/rp.go b/autorest/azure/rp.go index 86ce9f2b5..c6d39f686 100644 --- a/autorest/azure/rp.go +++ b/autorest/azure/rp.go @@ -47,11 +47,15 @@ func DoRetryWithRegistration(client autorest.Client) autorest.SendDecorator { if resp.StatusCode != http.StatusConflict || client.SkipResourceProviderRegistration { return resp, err } + var re RequestError - err = autorest.Respond( - resp, - autorest.ByUnmarshallingJSON(&re), - ) + if strings.Contains(r.Header.Get("Content-Type"), "xml") { + // XML errors (e.g. Storage Data Plane) only return the inner object + err = autorest.Respond(resp, autorest.ByUnmarshallingXML(&re.ServiceError)) + } else { + err = autorest.Respond(resp, autorest.ByUnmarshallingJSON(&re)) + } + if err != nil { return resp, err } From e6b2fe5b7dd11bcedabc7b39031c593e392e3d16 Mon Sep 17 00:00:00 2001 From: Tony Abboud Date: Mon, 21 Oct 2019 19:08:06 -0400 Subject: [PATCH 18/29] fix godoc comment for methods that are safe for concurrent use (#475) --- autorest/adal/token.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/autorest/adal/token.go b/autorest/adal/token.go index 7c7fca371..74273cddc 100644 --- a/autorest/adal/token.go +++ b/autorest/adal/token.go @@ -786,13 +786,13 @@ 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() @@ -800,13 +800,13 @@ func (spt *ServicePrincipalToken) RefreshWithContext(ctx context.Context) error } // 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() From a5c65562183c637ab7705758a200cb5c356f85c8 Mon Sep 17 00:00:00 2001 From: Tom Harvey Date: Tue, 22 Oct 2019 19:03:31 +0100 Subject: [PATCH 19/29] 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 --- autorest/authorization_storage.go | 301 +++++++++++++++++++++++++ autorest/authorization_storage_test.go | 122 ++++++++++ 2 files changed, 423 insertions(+) create mode 100644 autorest/authorization_storage.go create mode 100644 autorest/authorization_storage_test.go diff --git a/autorest/authorization_storage.go b/autorest/authorization_storage.go new file mode 100644 index 000000000..33e5f1270 --- /dev/null +++ b/autorest/authorization_storage.go @@ -0,0 +1,301 @@ +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 ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "fmt" + "net/http" + "net/url" + "sort" + "strings" + "time" +) + +// SharedKeyType defines the enumeration for the various shared key types. +// See https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key for details on the shared key types. +type SharedKeyType string + +const ( + // SharedKey is used to authorize against blobs, files and queues services. + SharedKey SharedKeyType = "sharedKey" + + // SharedKeyForTable is used to authorize against the table service. + SharedKeyForTable SharedKeyType = "sharedKeyTable" + + // SharedKeyLite is used to authorize against blobs, files and queues services. It's provided for + // backwards compatibility with API versions before 2009-09-19. Prefer SharedKey instead. + SharedKeyLite SharedKeyType = "sharedKeyLite" + + // SharedKeyLiteForTable is used to authorize against the table service. It's provided for + // backwards compatibility with older table API versions. Prefer SharedKeyForTable instead. + SharedKeyLiteForTable SharedKeyType = "sharedKeyLiteTable" +) + +const ( + headerAccept = "Accept" + headerAcceptCharset = "Accept-Charset" + headerContentEncoding = "Content-Encoding" + headerContentLength = "Content-Length" + headerContentMD5 = "Content-MD5" + headerContentLanguage = "Content-Language" + headerIfModifiedSince = "If-Modified-Since" + headerIfMatch = "If-Match" + headerIfNoneMatch = "If-None-Match" + headerIfUnmodifiedSince = "If-Unmodified-Since" + headerDate = "Date" + headerXMSDate = "X-Ms-Date" + headerXMSVersion = "x-ms-version" + headerRange = "Range" +) + +const storageEmulatorAccountName = "devstoreaccount1" + +// SharedKeyAuthorizer implements an authorization for Shared Key +// this can be used for interaction with Blob, File and Queue Storage Endpoints +type SharedKeyAuthorizer struct { + accountName string + accountKey []byte + keyType SharedKeyType +} + +// NewSharedKeyAuthorizer creates a SharedKeyAuthorizer using the provided credentials and shared key type. +func NewSharedKeyAuthorizer(accountName, accountKey string, keyType SharedKeyType) (*SharedKeyAuthorizer, error) { + key, err := base64.StdEncoding.DecodeString(accountKey) + if err != nil { + return nil, fmt.Errorf("malformed storage account key: %v", err) + } + return &SharedKeyAuthorizer{ + accountName: accountName, + accountKey: key, + keyType: keyType, + }, nil +} + +// WithAuthorization returns a PrepareDecorator that adds an HTTP Authorization header whose +// value is " " followed by the computed key. +// This can be used for the Blob, Queue, and File Services +// +// from: https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key +// You may use Shared Key authorization to authorize a request made against the +// 2009-09-19 version and later of the Blob and Queue services, +// and version 2014-02-14 and later of the File services. +func (sk *SharedKeyAuthorizer) 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 + } + + sk, err := buildSharedKey(sk.accountName, sk.accountKey, r, sk.keyType) + return Prepare(r, WithHeader(headerAuthorization, sk)) + }) + } +} + +func buildSharedKey(accName string, accKey []byte, req *http.Request, keyType SharedKeyType) (string, error) { + canRes, err := buildCanonicalizedResource(accName, req.URL.String(), keyType) + if err != nil { + return "", err + } + + if req.Header == nil { + req.Header = http.Header{} + } + + // ensure date is set + if req.Header.Get(headerDate) == "" && req.Header.Get(headerXMSDate) == "" { + date := time.Now().UTC().Format(http.TimeFormat) + req.Header.Set(headerXMSDate, date) + } + canString, err := buildCanonicalizedString(req.Method, req.Header, canRes, keyType) + if err != nil { + return "", err + } + return createAuthorizationHeader(accName, accKey, canString, keyType), nil +} + +func buildCanonicalizedResource(accountName, uri string, keyType SharedKeyType) (string, error) { + errMsg := "buildCanonicalizedResource error: %s" + u, err := url.Parse(uri) + if err != nil { + return "", fmt.Errorf(errMsg, err.Error()) + } + + cr := bytes.NewBufferString("") + if accountName != storageEmulatorAccountName { + cr.WriteString("/") + cr.WriteString(getCanonicalizedAccountName(accountName)) + } + + if len(u.Path) > 0 { + // Any portion of the CanonicalizedResource string that is derived from + // the resource's URI should be encoded exactly as it is in the URI. + // -- https://msdn.microsoft.com/en-gb/library/azure/dd179428.aspx + cr.WriteString(u.EscapedPath()) + } + + params, err := url.ParseQuery(u.RawQuery) + if err != nil { + return "", fmt.Errorf(errMsg, err.Error()) + } + + // See https://github.com/Azure/azure-storage-net/blob/master/Lib/Common/Core/Util/AuthenticationUtility.cs#L277 + if keyType == SharedKey { + if len(params) > 0 { + cr.WriteString("\n") + + keys := []string{} + for key := range params { + keys = append(keys, key) + } + sort.Strings(keys) + + completeParams := []string{} + for _, key := range keys { + if len(params[key]) > 1 { + sort.Strings(params[key]) + } + + completeParams = append(completeParams, fmt.Sprintf("%s:%s", key, strings.Join(params[key], ","))) + } + cr.WriteString(strings.Join(completeParams, "\n")) + } + } else { + // search for "comp" parameter, if exists then add it to canonicalizedresource + if v, ok := params["comp"]; ok { + cr.WriteString("?comp=" + v[0]) + } + } + + return string(cr.Bytes()), nil +} + +func getCanonicalizedAccountName(accountName string) string { + // since we may be trying to access a secondary storage account, we need to + // remove the -secondary part of the storage name + return strings.TrimSuffix(accountName, "-secondary") +} + +func buildCanonicalizedString(verb string, headers http.Header, canonicalizedResource string, keyType SharedKeyType) (string, error) { + contentLength := headers.Get(headerContentLength) + if contentLength == "0" { + contentLength = "" + } + date := headers.Get(headerDate) + if v := headers.Get(headerXMSDate); v != "" { + if keyType == SharedKey || keyType == SharedKeyLite { + date = "" + } else { + date = v + } + } + var canString string + switch keyType { + case SharedKey: + canString = strings.Join([]string{ + verb, + headers.Get(headerContentEncoding), + headers.Get(headerContentLanguage), + contentLength, + headers.Get(headerContentMD5), + headers.Get(headerContentType), + date, + headers.Get(headerIfModifiedSince), + headers.Get(headerIfMatch), + headers.Get(headerIfNoneMatch), + headers.Get(headerIfUnmodifiedSince), + headers.Get(headerRange), + buildCanonicalizedHeader(headers), + canonicalizedResource, + }, "\n") + case SharedKeyForTable: + canString = strings.Join([]string{ + verb, + headers.Get(headerContentMD5), + headers.Get(headerContentType), + date, + canonicalizedResource, + }, "\n") + case SharedKeyLite: + canString = strings.Join([]string{ + verb, + headers.Get(headerContentMD5), + headers.Get(headerContentType), + date, + buildCanonicalizedHeader(headers), + canonicalizedResource, + }, "\n") + case SharedKeyLiteForTable: + canString = strings.Join([]string{ + date, + canonicalizedResource, + }, "\n") + default: + return "", fmt.Errorf("key type '%s' is not supported", keyType) + } + return canString, nil +} + +func buildCanonicalizedHeader(headers http.Header) string { + cm := make(map[string]string) + + for k := range headers { + headerName := strings.TrimSpace(strings.ToLower(k)) + if strings.HasPrefix(headerName, "x-ms-") { + cm[headerName] = headers.Get(k) + } + } + + if len(cm) == 0 { + return "" + } + + keys := []string{} + for key := range cm { + keys = append(keys, key) + } + + sort.Strings(keys) + + ch := bytes.NewBufferString("") + + for _, key := range keys { + ch.WriteString(key) + ch.WriteRune(':') + ch.WriteString(cm[key]) + ch.WriteRune('\n') + } + + return strings.TrimSuffix(string(ch.Bytes()), "\n") +} + +func createAuthorizationHeader(accountName string, accountKey []byte, canonicalizedString string, keyType SharedKeyType) string { + h := hmac.New(sha256.New, accountKey) + h.Write([]byte(canonicalizedString)) + signature := base64.StdEncoding.EncodeToString(h.Sum(nil)) + var key string + switch keyType { + case SharedKey, SharedKeyForTable: + key = "SharedKey" + case SharedKeyLite, SharedKeyLiteForTable: + key = "SharedKeyLite" + } + return fmt.Sprintf("%s %s:%s", key, getCanonicalizedAccountName(accountName), signature) +} diff --git a/autorest/authorization_storage_test.go b/autorest/authorization_storage_test.go new file mode 100644 index 000000000..ae7c31b5f --- /dev/null +++ b/autorest/authorization_storage_test.go @@ -0,0 +1,122 @@ +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" + "testing" +) + +func TestNewSharedKeyAuthorizer(t *testing.T) { + auth, err := NewSharedKeyAuthorizer("golangrocksonazure", "YmFy", SharedKey) + if err != nil { + t.Fatalf("create shared key authorizer: %v", err) + } + req, err := http.NewRequest(http.MethodGet, "https://golangrocksonazure.blob.core.windows.net/some/blob.dat", nil) + if err != nil { + t.Fatalf("create HTTP request: %v", err) + } + req.Header.Add(headerAcceptCharset, "UTF-8") + req.Header.Add(headerContentType, "application/json") + req.Header.Add(headerXMSDate, "Wed, 23 Sep 2015 16:40:05 GMT") + req.Header.Add(headerContentLength, "0") + req.Header.Add(headerXMSVersion, "2015-02-21") + req.Header.Add(headerAccept, "application/json;odata=nometadata") + req, err = Prepare(req, auth.WithAuthorization()) + if err != nil { + t.Fatalf("prepare HTTP request: %v", err) + } + const expected = "SharedKey golangrocksonazure:nYRqgbumDOTPs+Vv1FLH+hm0KPjwwt+Fmj/i16W+lO0=" + if auth := req.Header.Get(headerAuthorization); auth != expected { + t.Fatalf("expected: %s, go %s", expected, auth) + } +} + +func TestNewSharedKeyForTableAuthorizer(t *testing.T) { + auth, err := NewSharedKeyAuthorizer("golangrocksonazure", "YmFy", SharedKeyForTable) + if err != nil { + t.Fatalf("create shared key authorizer: %v", err) + } + req, err := http.NewRequest(http.MethodGet, "https://golangrocksonazure.table.core.windows.net/tquery()", nil) + if err != nil { + t.Fatalf("create HTTP request: %v", err) + } + req.Header.Add(headerAcceptCharset, "UTF-8") + req.Header.Add(headerContentType, "application/json") + req.Header.Add(headerXMSDate, "Wed, 23 Sep 2015 16:40:05 GMT") + req.Header.Add(headerContentLength, "0") + req.Header.Add(headerXMSVersion, "2015-02-21") + req.Header.Add(headerAccept, "application/json;odata=nometadata") + req, err = Prepare(req, auth.WithAuthorization()) + if err != nil { + t.Fatalf("prepare HTTP request: %v", err) + } + const expected = "SharedKey golangrocksonazure:73oeIBA2dulLhOBdAlM3U0+DKIWS0UW6InBWCHpOY50=" + if auth := req.Header.Get(headerAuthorization); auth != expected { + t.Fatalf("expected: %s, go %s", expected, auth) + } +} + +func TestNewSharedKeyLiteAuthorizer(t *testing.T) { + auth, err := NewSharedKeyAuthorizer("golangrocksonazure", "YmFy", SharedKeyLite) + if err != nil { + t.Fatalf("create shared key authorizer: %v", err) + } + + req, err := http.NewRequest(http.MethodGet, "https://golangrocksonazure.file.core.windows.net/some/file.dat", nil) + if err != nil { + t.Fatalf("create HTTP request: %v", err) + } + req.Header.Add(headerAcceptCharset, "UTF-8") + req.Header.Add(headerContentType, "application/json") + req.Header.Add(headerXMSDate, "Wed, 23 Sep 2015 16:40:05 GMT") + req.Header.Add(headerContentLength, "0") + req.Header.Add(headerXMSVersion, "2015-02-21") + req.Header.Add(headerAccept, "application/json;odata=nometadata") + req, err = Prepare(req, auth.WithAuthorization()) + if err != nil { + t.Fatalf("prepare HTTP request: %v", err) + } + const expected = "SharedKeyLite golangrocksonazure:0VODf/mHRDa7lMShzTKbow7lxptaIZ0qIAcVD0lG9PE=" + if auth := req.Header.Get(headerAuthorization); auth != expected { + t.Fatalf("expected: %s, go %s", expected, auth) + } +} + +func TestNewSharedKeyLiteForTableAuthorizer(t *testing.T) { + auth, err := NewSharedKeyAuthorizer("golangrocksonazure", "YmFy", SharedKeyLiteForTable) + if err != nil { + t.Fatalf("create shared key authorizer: %v", err) + } + + req, err := http.NewRequest(http.MethodGet, "https://golangrocksonazure.table.core.windows.net/tquery()", nil) + if err != nil { + t.Fatalf("create HTTP request: %v", err) + } + req.Header.Add(headerAcceptCharset, "UTF-8") + req.Header.Add(headerContentType, "application/json") + req.Header.Add(headerXMSDate, "Wed, 23 Sep 2015 16:40:05 GMT") + req.Header.Add(headerContentLength, "0") + req.Header.Add(headerXMSVersion, "2015-02-21") + req.Header.Add(headerAccept, "application/json;odata=nometadata") + req, err = Prepare(req, auth.WithAuthorization()) + if err != nil { + t.Fatalf("prepare HTTP request: %v", err) + } + const expected = "SharedKeyLite golangrocksonazure:NusXSFXAvHqr6EQNXnZZ50CvU1sX0iP/FFDHehnixLc=" + if auth := req.Header.Get(headerAuthorization); auth != expected { + t.Fatalf("expected: %s, go %s", expected, auth) + } +} From 5f1f2ad627905597f6ab689c6dd39e32ad3c25c0 Mon Sep 17 00:00:00 2001 From: Tom Harvey Date: Wed, 23 Oct 2019 16:09:57 +0100 Subject: [PATCH 20/29] 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 --- autorest/authorization_sas.go | 67 +++++++++++++++++ autorest/authorization_sas_test.go | 113 +++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 autorest/authorization_sas.go create mode 100644 autorest/authorization_sas_test.go diff --git a/autorest/authorization_sas.go b/autorest/authorization_sas.go new file mode 100644 index 000000000..89a659cb6 --- /dev/null +++ b/autorest/authorization_sas.go @@ -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) + }) + } +} diff --git a/autorest/authorization_sas_test.go b/autorest/authorization_sas_test.go new file mode 100644 index 000000000..667dad9d4 --- /dev/null +++ b/autorest/authorization_sas_test.go @@ -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!") + } + } +} From 7820109f298d33170d298cc1fe20b83b3b4b1baa Mon Sep 17 00:00:00 2001 From: Tom Harvey Date: Wed, 23 Oct 2019 18:22:32 +0100 Subject: [PATCH 21/29] token: support for a custom refresh func (#476) * token: support for a custom refresh func * pass closures by value * minor clean-up --- autorest/adal/token.go | 26 +++++++++++++++++++++---- autorest/adal/token_test.go | 38 +++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/autorest/adal/token.go b/autorest/adal/token.go index 74273cddc..33bbd6ea1 100644 --- a/autorest/adal/token.go +++ b/autorest/adal/token.go @@ -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 { @@ -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 } @@ -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) @@ -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) diff --git a/autorest/adal/token_test.go b/autorest/adal/token_test.go index d2cb53f69..d20a475f4 100644 --- a/autorest/adal/token_test.go +++ b/autorest/adal/token_test.go @@ -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() @@ -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() From f8ee05f61fbe158fa966782edf89e0c1b41ee2eb Mon Sep 17 00:00:00 2001 From: Lars Lehtonen Date: Wed, 30 Oct 2019 08:23:22 -0700 Subject: [PATCH 22/29] Fix Dropped Errors (#480) * autorest: fix dropped errror * autorest/adal: fix dropped test error --- autorest/adal/token_test.go | 3 +++ autorest/authorization_storage.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/autorest/adal/token_test.go b/autorest/adal/token_test.go index d20a475f4..8f4ec25c4 100644 --- a/autorest/adal/token_test.go +++ b/autorest/adal/token_test.go @@ -978,6 +978,9 @@ func TestNewMultiTenantServicePrincipalToken(t *testing.T) { t.Fatalf("autorest/adal: unexpected error while creating multitenant config: %v", err) } mt, err := NewMultiTenantServicePrincipalToken(cfg, "clientID", "superSecret", "resource") + if err != nil { + t.Fatalf("autorest/adal: unexpected error while creating multitenant service principal token: %v", err) + } if !strings.Contains(mt.PrimaryToken.inner.OauthConfig.AuthorizeEndpoint.String(), TestTenantID) { t.Fatal("didn't find primary tenant ID in primary SPT") } diff --git a/autorest/authorization_storage.go b/autorest/authorization_storage.go index 33e5f1270..b844a3df4 100644 --- a/autorest/authorization_storage.go +++ b/autorest/authorization_storage.go @@ -104,6 +104,9 @@ func (sk *SharedKeyAuthorizer) WithAuthorization() PrepareDecorator { } sk, err := buildSharedKey(sk.accountName, sk.accountKey, r, sk.keyType) + if err != nil { + return r, err + } return Prepare(r, WithHeader(headerAuthorization, sk)) }) } From be77dbdb88ec039d20e3cf9a7533c5b94753f953 Mon Sep 17 00:00:00 2001 From: Maxim Fominykh Date: Wed, 5 Feb 2020 02:40:49 +0500 Subject: [PATCH 23/29] Duration order consistency when multiplying number by time unit (#499) --- autorest/adal/token.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autorest/adal/token.go b/autorest/adal/token.go index 33bbd6ea1..566e13d0e 100644 --- a/autorest/adal/token.go +++ b/autorest/adal/token.go @@ -248,7 +248,7 @@ func (secret *ServicePrincipalCertificateSecret) SignJwt(spt *ServicePrincipalTo "sub": spt.inner.ClientID, "jti": base64.URLEncoding.EncodeToString(jti), "nbf": time.Now().Unix(), - "exp": time.Now().Add(time.Hour * 24).Unix(), + "exp": time.Now().Add(24 * time.Hour).Unix(), } signedString, err := token.SignedString(secret.PrivateKey) From ee2a6861b1e587d5faf21123d5fdb8bc19220b7a Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Wed, 5 Feb 2020 08:57:43 -0800 Subject: [PATCH 24/29] Drain response bodies (#432) The retry helpers and a few other methods weren't reading and closing response bodies leading to connection leaks. --- autorest/adal/token.go | 5 +++++ autorest/authorization.go | 31 ++++++++++++++++--------------- autorest/azure/async.go | 12 +++++++++++- autorest/sender.go | 3 +++ autorest/utility.go | 11 +++++++++++ autorest/utility_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 85 insertions(+), 16 deletions(-) diff --git a/autorest/adal/token.go b/autorest/adal/token.go index 566e13d0e..b65b2c8b2 100644 --- a/autorest/adal/token.go +++ b/autorest/adal/token.go @@ -24,6 +24,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/ioutil" "math" "net/http" @@ -972,6 +973,10 @@ func retryForIMDS(sender Sender, req *http.Request, maxAttempts int) (resp *http delay := time.Duration(0) for attempt < maxAttempts { + if resp != nil && resp.Body != nil { + io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() + } resp, err = sender.Do(req) // we want to retry if err is not nil or the status code is in the list of retry codes if err == nil && !responseHasStatusCode(resp, retries...) { diff --git a/autorest/authorization.go b/autorest/authorization.go index 54e87b5b6..f43e1a6ed 100644 --- a/autorest/authorization.go +++ b/autorest/authorization.go @@ -171,20 +171,21 @@ func (bacb *BearerAuthorizerCallback) WithAuthorization() PrepareDecorator { removeRequestBody(&rCopy) resp, err := bacb.sender.Do(&rCopy) - if err == nil && resp.StatusCode == 401 { - defer resp.Body.Close() - if hasBearerChallenge(resp) { - bc, err := newBearerChallenge(resp) + if err != nil { + return r, err + } + DrainResponseBody(resp) + if resp.StatusCode == 401 && hasBearerChallenge(resp.Header) { + bc, err := newBearerChallenge(resp.Header) + if err != nil { + return r, err + } + if bacb.callback != nil { + ba, err := bacb.callback(bc.values[tenantID], bc.values["resource"]) if err != nil { return r, err } - if bacb.callback != nil { - ba, err := bacb.callback(bc.values[tenantID], bc.values["resource"]) - if err != nil { - return r, err - } - return Prepare(r, ba.WithAuthorization()) - } + return Prepare(r, ba.WithAuthorization()) } } } @@ -194,8 +195,8 @@ func (bacb *BearerAuthorizerCallback) WithAuthorization() PrepareDecorator { } // returns true if the HTTP response contains a bearer challenge -func hasBearerChallenge(resp *http.Response) bool { - authHeader := resp.Header.Get(bearerChallengeHeader) +func hasBearerChallenge(header http.Header) bool { + authHeader := header.Get(bearerChallengeHeader) if len(authHeader) == 0 || strings.Index(authHeader, bearer) < 0 { return false } @@ -206,8 +207,8 @@ type bearerChallenge struct { values map[string]string } -func newBearerChallenge(resp *http.Response) (bc bearerChallenge, err error) { - challenge := strings.TrimSpace(resp.Header.Get(bearerChallengeHeader)) +func newBearerChallenge(header http.Header) (bc bearerChallenge, err error) { + challenge := strings.TrimSpace(header.Get(bearerChallengeHeader)) trimmedChallenge := challenge[len(bearer)+1:] // challenge is a set of key=value pairs that are comma delimited diff --git a/autorest/azure/async.go b/autorest/azure/async.go index 1cb41cbeb..c5fc511f6 100644 --- a/autorest/azure/async.go +++ b/autorest/azure/async.go @@ -258,7 +258,17 @@ func (f Future) GetResult(sender autorest.Sender) (*http.Response, error) { if err != nil { return nil, err } - return sender.Do(req) + resp, err := sender.Do(req) + if err == nil && resp.Body != nil { + // copy the body and close it so callers don't have to + defer resp.Body.Close() + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return resp, err + } + resp.Body = ioutil.NopCloser(bytes.NewReader(b)) + } + return resp, err } type pollingTracker interface { diff --git a/autorest/sender.go b/autorest/sender.go index 5e595d7b1..5c07e82bb 100644 --- a/autorest/sender.go +++ b/autorest/sender.go @@ -243,6 +243,7 @@ func DoRetryForAttempts(attempts int, backoff time.Duration) SendDecorator { if err != nil { return resp, err } + DrainResponseBody(resp) resp, err = s.Do(rr.Request()) if err == nil { return resp, err @@ -288,6 +289,7 @@ func doRetryForStatusCodesImpl(s Sender, r *http.Request, count429 bool, attempt if err != nil { return } + DrainResponseBody(resp) resp, err = s.Do(rr.Request()) // we want to retry if err is not nil (e.g. transient network failure). note that for failed authentication // resp and err will both have a value, so in this case we don't want to retry as it will never succeed. @@ -347,6 +349,7 @@ func DoRetryForDuration(d time.Duration, backoff time.Duration) SendDecorator { if err != nil { return resp, err } + DrainResponseBody(resp) resp, err = s.Do(rr.Request()) if err == nil { return resp, err diff --git a/autorest/utility.go b/autorest/utility.go index 27f824f54..67baab2ce 100644 --- a/autorest/utility.go +++ b/autorest/utility.go @@ -20,6 +20,7 @@ import ( "encoding/xml" "fmt" "io" + "io/ioutil" "net" "net/http" "net/url" @@ -226,3 +227,13 @@ func IsTemporaryNetworkError(err error) bool { } return false } + +// DrainResponseBody reads the response body then closes it. +func DrainResponseBody(resp *http.Response) error { + if resp != nil && resp.Body != nil { + _, err := io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() + return err + } + return nil +} diff --git a/autorest/utility_test.go b/autorest/utility_test.go index 2c6ed95c1..ab85c784c 100644 --- a/autorest/utility_test.go +++ b/autorest/utility_test.go @@ -20,6 +20,7 @@ import ( "encoding/xml" "errors" "fmt" + "io" "net/http" "net/url" "reflect" @@ -469,3 +470,41 @@ func withErrorRespondDecorator(e *error) RespondDecorator { }) } } + +type mockDrain struct { + read bool + closed bool +} + +func (md *mockDrain) Read(b []byte) (int, error) { + md.read = true + b = append(b, 0xff) + return 1, io.EOF +} + +func (md *mockDrain) Close() error { + md.closed = true + return nil +} + +func TestDrainResponseBody(t *testing.T) { + err := DrainResponseBody(nil) + if err != nil { + t.Fatalf("expected nil error, got %v", err) + } + err = DrainResponseBody(&http.Response{}) + if err != nil { + t.Fatalf("expected nil error, got %v", err) + } + md := &mockDrain{} + err = DrainResponseBody(&http.Response{Body: md}) + if err != nil { + t.Fatalf("expected nil error, got %v", err) + } + if !md.closed { + t.Fatal("mockDrain wasn't closed") + } + if !md.read { + t.Fatal("mockDrain wasn't read") + } +} From 4aad125709258340e37f1b872806fc933cbc13db Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Thu, 6 Feb 2020 10:56:27 -0800 Subject: [PATCH 25/29] Enable exponential back-off when retrying on 429 (#503) * Enable exponential back-off when retrying on 429 * enforce a 2-minute cap on delays if there isn't one * updated comment * fix type-o --- autorest/sender.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/autorest/sender.go b/autorest/sender.go index 5c07e82bb..a07670b8c 100644 --- a/autorest/sender.go +++ b/autorest/sender.go @@ -284,7 +284,7 @@ func DoRetryForStatusCodesWithCap(attempts int, backoff, cap time.Duration, code func doRetryForStatusCodesImpl(s Sender, r *http.Request, count429 bool, attempts int, backoff, cap time.Duration, codes ...int) (resp *http.Response, err error) { rr := NewRetriableRequest(r) // Increment to add the first call (attempts denotes number of retries) - for attempt := 0; attempt < attempts+1; { + for attempt, delayCount := 0, 0; attempt < attempts+1; { err = rr.Prepare() if err != nil { return @@ -297,7 +297,13 @@ func doRetryForStatusCodesImpl(s Sender, r *http.Request, count429 bool, attempt return resp, err } delayed := DelayWithRetryAfter(resp, r.Context().Done()) - if !delayed && !DelayForBackoffWithCap(backoff, cap, attempt, r.Context().Done()) { + // enforce a 2 minute cap between requests when 429 status codes are + // not going to be counted as an attempt and when the cap is 0. + // this should only happen in the absence of a retry-after header. + if !count429 && cap == 0 { + cap = 2 * time.Minute + } + if !delayed && !DelayForBackoffWithCap(backoff, cap, delayCount, r.Context().Done()) { return resp, r.Context().Err() } // when count429 == false don't count a 429 against the number @@ -305,6 +311,9 @@ func doRetryForStatusCodesImpl(s Sender, r *http.Request, count429 bool, attempt if count429 || (resp == nil || resp.StatusCode != http.StatusTooManyRequests) { attempt++ } + // delay count is tracked separately from attempts to + // ensure that 429 participates in exponential back-off + delayCount++ } return resp, err } From 5ac3904e49968a92f7a1b6589d43afc733093726 Mon Sep 17 00:00:00 2001 From: alespour <42931850+alespour@users.noreply.github.com> Date: Fri, 5 Jun 2020 19:30:18 +0200 Subject: [PATCH 26/29] Expose OAuth token provider for use outside autorest (#520) * feat: extract token creation to public method for MSI auth * Add getter for token provider on BearerAuthorizer --- autorest/authorization.go | 5 +++++ autorest/azure/auth/auth.go | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/autorest/authorization.go b/autorest/authorization.go index 5802f6a4b..15138b642 100644 --- a/autorest/authorization.go +++ b/autorest/authorization.go @@ -138,6 +138,11 @@ func (ba *BearerAuthorizer) WithAuthorization() PrepareDecorator { } } +// TokenProvider returns OAuthTokenProvider so that it can be used for authorization outside the REST. +func (ba *BearerAuthorizer) TokenProvider() adal.OAuthTokenProvider { + return ba.tokenProvider +} + // BearerAuthorizerCallbackFunc is the authentication callback signature. type BearerAuthorizerCallbackFunc func(tenantID, resource string) (*BearerAuthorizer, error) diff --git a/autorest/azure/auth/auth.go b/autorest/azure/auth/auth.go index 74898dfc3..2b37e0d06 100644 --- a/autorest/azure/auth/auth.go +++ b/autorest/azure/auth/auth.go @@ -713,8 +713,8 @@ type MSIConfig struct { ClientID string } -// Authorizer gets the authorizer from MSI. -func (mc MSIConfig) Authorizer() (autorest.Authorizer, error) { +// ServicePrincipalToken creates a ServicePrincipalToken from MSI. +func (mc MSIConfig) ServicePrincipalToken() (*adal.ServicePrincipalToken, error) { msiEndpoint, err := adal.GetMSIEndpoint() if err != nil { return nil, err @@ -733,5 +733,15 @@ func (mc MSIConfig) Authorizer() (autorest.Authorizer, error) { } } + return spToken, nil +} + +// Authorizer gets the authorizer from MSI. +func (mc MSIConfig) Authorizer() (autorest.Authorizer, error) { + spToken, err := mc.ServicePrincipalToken() + if err != nil { + return nil, err + } + return autorest.NewBearerAuthorizer(spToken), nil } From 18c897ad0b22d59ef0426f793bcb2d6dc2c0f072 Mon Sep 17 00:00:00 2001 From: Mark Severson Date: Tue, 23 Jun 2020 12:21:49 -0600 Subject: [PATCH 27/29] Fix Go module ambiguous import errors (#528) * Fix Go module ambiguous import errors This is an extension of the mitigations introduced in #455. Unfortunately, the original mitigations didn't address the primary cause of ambiguous import errors: the github.com/Azure/go-autorest module. The issue stems from the fact that old versions of the root module (github.com/Azure/go-autorest) provide the same packages as the newer submodules. To correct this situation, the _root module_ needs to be upgraded to a version that no longer provides those packages (a version where the submodules are present). Fortunately, the submodules can be leveraged to provide the necessary version bump. See: https://github.com/Azure/go-autorest/issues/414#issuecomment-521751306 ---- Caveat: in order for this to work, an importable version of the root package needs to be referenceable. PR #527 makes the root package importable. The go.mod files assume that this importable version will be referenceable as v14.2.0. If the version where the importable package is available ends up being different, these files will need to be updated. See also: #395, #413, #414, #455, #481, #524 * Update go.sum files Co-authored-by: Joel Hendrix --- autorest/adal/go.mod | 2 +- autorest/adal/go.sum | 2 ++ autorest/adal/go_mod_tidy_hack.go | 4 ++-- autorest/azure/auth/go.mod | 1 + autorest/azure/auth/go.sum | 2 ++ autorest/azure/auth/go_mod_tidy_hack.go | 4 ++-- autorest/azure/cli/go.mod | 2 +- autorest/azure/cli/go.sum | 2 ++ autorest/azure/cli/go_mod_tidy_hack.go | 4 ++-- autorest/date/go.mod | 2 +- autorest/date/go.sum | 18 ++---------------- autorest/date/go_mod_tidy_hack.go | 4 ++-- autorest/go.mod | 1 + autorest/go.sum | 2 ++ autorest/go_mod_tidy_hack.go | 24 ++++++++++++++++++++++++ autorest/mocks/go.mod | 2 +- autorest/mocks/go.sum | 18 ++---------------- autorest/mocks/go_mod_tidy_hack.go | 4 ++-- autorest/to/go.mod | 2 +- autorest/to/go.sum | 19 ++----------------- autorest/to/go_mod_tidy_hack.go | 4 ++-- autorest/validation/go.mod | 2 +- autorest/validation/go.sum | 19 ++----------------- autorest/validation/go_mod_tidy_hack.go | 4 ++-- logger/go.mod | 2 ++ logger/go.sum | 2 ++ logger/go_mod_tidy_hack.go | 24 ++++++++++++++++++++++++ tracing/go.mod | 2 ++ tracing/go.sum | 2 ++ tracing/go_mod_tidy_hack.go | 24 ++++++++++++++++++++++++ 30 files changed, 118 insertions(+), 86 deletions(-) create mode 100644 autorest/go_mod_tidy_hack.go create mode 100644 logger/go.sum create mode 100644 logger/go_mod_tidy_hack.go create mode 100644 tracing/go.sum create mode 100644 tracing/go_mod_tidy_hack.go diff --git a/autorest/adal/go.mod b/autorest/adal/go.mod index a030eb42d..e7125344c 100644 --- a/autorest/adal/go.mod +++ b/autorest/adal/go.mod @@ -3,7 +3,7 @@ module github.com/Azure/go-autorest/autorest/adal go 1.12 require ( - github.com/Azure/go-autorest/autorest v0.9.0 + github.com/Azure/go-autorest v14.2.0+incompatible github.com/Azure/go-autorest/autorest/date v0.2.0 github.com/Azure/go-autorest/autorest/mocks v0.3.0 github.com/Azure/go-autorest/tracing v0.5.0 diff --git a/autorest/adal/go.sum b/autorest/adal/go.sum index e43cf6498..59296a5a7 100644 --- a/autorest/adal/go.sum +++ b/autorest/adal/go.sum @@ -1,3 +1,5 @@ +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= diff --git a/autorest/adal/go_mod_tidy_hack.go b/autorest/adal/go_mod_tidy_hack.go index 28a4bfc4c..7551b7923 100644 --- a/autorest/adal/go_mod_tidy_hack.go +++ b/autorest/adal/go_mod_tidy_hack.go @@ -16,9 +16,9 @@ package adal // See the License for the specific language governing permissions and // limitations under the License. -// This file, and the github.com/Azure/go-autorest/autorest import, won't actually become part of +// This file, and the github.com/Azure/go-autorest import, won't actually become part of // the resultant binary. // Necessary for safely adding multi-module repo. // See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository -import _ "github.com/Azure/go-autorest/autorest" +import _ "github.com/Azure/go-autorest" diff --git a/autorest/azure/auth/go.mod b/autorest/azure/auth/go.mod index 4bebbdc24..d38dcbbe0 100644 --- a/autorest/azure/auth/go.mod +++ b/autorest/azure/auth/go.mod @@ -3,6 +3,7 @@ module github.com/Azure/go-autorest/autorest/azure/auth go 1.12 require ( + github.com/Azure/go-autorest v14.2.0+incompatible github.com/Azure/go-autorest/autorest v0.9.3 github.com/Azure/go-autorest/autorest/adal v0.8.1 github.com/Azure/go-autorest/autorest/azure/cli v0.3.1 diff --git a/autorest/azure/auth/go.sum b/autorest/azure/auth/go.sum index 9ea3fa575..65edc27da 100644 --- a/autorest/azure/auth/go.sum +++ b/autorest/azure/auth/go.sum @@ -1,3 +1,5 @@ +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= github.com/Azure/go-autorest/autorest v0.9.3 h1:OZEIaBbMdUE/Js+BQKlpO81XlISgipr6yDJ+PSwsgi4= diff --git a/autorest/azure/auth/go_mod_tidy_hack.go b/autorest/azure/auth/go_mod_tidy_hack.go index 2f09cd177..38e4900ad 100644 --- a/autorest/azure/auth/go_mod_tidy_hack.go +++ b/autorest/azure/auth/go_mod_tidy_hack.go @@ -16,9 +16,9 @@ package auth // See the License for the specific language governing permissions and // limitations under the License. -// This file, and the github.com/Azure/go-autorest/autorest import, won't actually become part of +// This file, and the github.com/Azure/go-autorest import, won't actually become part of // the resultant binary. // Necessary for safely adding multi-module repo. // See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository -import _ "github.com/Azure/go-autorest/autorest" +import _ "github.com/Azure/go-autorest" diff --git a/autorest/azure/cli/go.mod b/autorest/azure/cli/go.mod index a58302914..14397e9a6 100644 --- a/autorest/azure/cli/go.mod +++ b/autorest/azure/cli/go.mod @@ -3,7 +3,7 @@ module github.com/Azure/go-autorest/autorest/azure/cli go 1.12 require ( - github.com/Azure/go-autorest/autorest v0.9.0 + github.com/Azure/go-autorest v14.2.0+incompatible github.com/Azure/go-autorest/autorest/adal v0.8.0 github.com/Azure/go-autorest/autorest/date v0.2.0 github.com/dimchansky/utfbom v1.1.0 diff --git a/autorest/azure/cli/go.sum b/autorest/azure/cli/go.sum index 542806b94..977fc8a2d 100644 --- a/autorest/azure/cli/go.sum +++ b/autorest/azure/cli/go.sum @@ -1,3 +1,5 @@ +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= github.com/Azure/go-autorest/autorest/adal v0.5.0 h1:q2gDruN08/guU9vAjuPWff0+QIrpH6ediguzdAzXAUU= diff --git a/autorest/azure/cli/go_mod_tidy_hack.go b/autorest/azure/cli/go_mod_tidy_hack.go index 618bed392..861ce2984 100644 --- a/autorest/azure/cli/go_mod_tidy_hack.go +++ b/autorest/azure/cli/go_mod_tidy_hack.go @@ -16,9 +16,9 @@ package cli // See the License for the specific language governing permissions and // limitations under the License. -// This file, and the github.com/Azure/go-autorest/autorest import, won't actually become part of +// This file, and the github.com/Azure/go-autorest import, won't actually become part of // the resultant binary. // Necessary for safely adding multi-module repo. // See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository -import _ "github.com/Azure/go-autorest/autorest" +import _ "github.com/Azure/go-autorest" diff --git a/autorest/date/go.mod b/autorest/date/go.mod index 3adc4804c..f88ecc402 100644 --- a/autorest/date/go.mod +++ b/autorest/date/go.mod @@ -2,4 +2,4 @@ module github.com/Azure/go-autorest/autorest/date go 1.12 -require github.com/Azure/go-autorest/autorest v0.9.0 +require github.com/Azure/go-autorest v14.2.0+incompatible diff --git a/autorest/date/go.sum b/autorest/date/go.sum index 9e2ee7a94..1fc56a962 100644 --- a/autorest/date/go.sum +++ b/autorest/date/go.sum @@ -1,16 +1,2 @@ -github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs= -github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= -github.com/Azure/go-autorest/autorest/adal v0.5.0 h1:q2gDruN08/guU9vAjuPWff0+QIrpH6ediguzdAzXAUU= -github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= -github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= -github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/autorest/mocks v0.2.0 h1:Ww5g4zThfD/6cLb4z6xxgeyDa7QDkizMkJKe0ysZXp0= -github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY= -github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= -github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k= -github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= diff --git a/autorest/date/go_mod_tidy_hack.go b/autorest/date/go_mod_tidy_hack.go index 55adf930f..4e0543207 100644 --- a/autorest/date/go_mod_tidy_hack.go +++ b/autorest/date/go_mod_tidy_hack.go @@ -16,9 +16,9 @@ package date // See the License for the specific language governing permissions and // limitations under the License. -// This file, and the github.com/Azure/go-autorest/autorest import, won't actually become part of +// This file, and the github.com/Azure/go-autorest import, won't actually become part of // the resultant binary. // Necessary for safely adding multi-module repo. // See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository -import _ "github.com/Azure/go-autorest/autorest" +import _ "github.com/Azure/go-autorest" diff --git a/autorest/go.mod b/autorest/go.mod index 499c56de4..035843d2c 100644 --- a/autorest/go.mod +++ b/autorest/go.mod @@ -3,6 +3,7 @@ module github.com/Azure/go-autorest/autorest go 1.12 require ( + github.com/Azure/go-autorest v14.2.0+incompatible github.com/Azure/go-autorest/autorest/adal v0.8.2 github.com/Azure/go-autorest/autorest/mocks v0.3.0 github.com/Azure/go-autorest/logger v0.1.0 diff --git a/autorest/go.sum b/autorest/go.sum index 37398d1d4..865e024d4 100644 --- a/autorest/go.sum +++ b/autorest/go.sum @@ -1,3 +1,5 @@ +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= github.com/Azure/go-autorest/autorest/adal v0.5.0 h1:q2gDruN08/guU9vAjuPWff0+QIrpH6ediguzdAzXAUU= github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= diff --git a/autorest/go_mod_tidy_hack.go b/autorest/go_mod_tidy_hack.go new file mode 100644 index 000000000..da65e1041 --- /dev/null +++ b/autorest/go_mod_tidy_hack.go @@ -0,0 +1,24 @@ +// +build modhack + +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. + +// This file, and the github.com/Azure/go-autorest import, won't actually become part of +// the resultant binary. + +// Necessary for safely adding multi-module repo. +// See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository +import _ "github.com/Azure/go-autorest" diff --git a/autorest/mocks/go.mod b/autorest/mocks/go.mod index f1dfdbeb5..c3f6b7e86 100644 --- a/autorest/mocks/go.mod +++ b/autorest/mocks/go.mod @@ -2,4 +2,4 @@ module github.com/Azure/go-autorest/autorest/mocks go 1.12 -require github.com/Azure/go-autorest/autorest v0.9.0 +require github.com/Azure/go-autorest v14.2.0+incompatible diff --git a/autorest/mocks/go.sum b/autorest/mocks/go.sum index 7d96b7ed8..1fc56a962 100644 --- a/autorest/mocks/go.sum +++ b/autorest/mocks/go.sum @@ -1,16 +1,2 @@ -github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs= -github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= -github.com/Azure/go-autorest/autorest/adal v0.5.0 h1:q2gDruN08/guU9vAjuPWff0+QIrpH6ediguzdAzXAUU= -github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= -github.com/Azure/go-autorest/autorest/date v0.1.0 h1:YGrhWfrgtFs84+h0o46rJrlmsZtyZRg470CqAXTZaGM= -github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= -github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY= -github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= -github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k= -github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= diff --git a/autorest/mocks/go_mod_tidy_hack.go b/autorest/mocks/go_mod_tidy_hack.go index b828eed70..85a4baf10 100644 --- a/autorest/mocks/go_mod_tidy_hack.go +++ b/autorest/mocks/go_mod_tidy_hack.go @@ -16,9 +16,9 @@ package mocks // See the License for the specific language governing permissions and // limitations under the License. -// This file, and the github.com/Azure/go-autorest/autorest import, won't actually become part of +// This file, and the github.com/Azure/go-autorest import, won't actually become part of // the resultant binary. // Necessary for safely adding multi-module repo. // See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository -import _ "github.com/Azure/go-autorest/autorest" +import _ "github.com/Azure/go-autorest" diff --git a/autorest/to/go.mod b/autorest/to/go.mod index 48fd8c6e5..8fd041e2b 100644 --- a/autorest/to/go.mod +++ b/autorest/to/go.mod @@ -2,4 +2,4 @@ module github.com/Azure/go-autorest/autorest/to go 1.12 -require github.com/Azure/go-autorest/autorest v0.9.0 +require github.com/Azure/go-autorest v14.2.0+incompatible diff --git a/autorest/to/go.sum b/autorest/to/go.sum index d7ee6b462..1fc56a962 100644 --- a/autorest/to/go.sum +++ b/autorest/to/go.sum @@ -1,17 +1,2 @@ -github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs= -github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= -github.com/Azure/go-autorest/autorest/adal v0.5.0 h1:q2gDruN08/guU9vAjuPWff0+QIrpH6ediguzdAzXAUU= -github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= -github.com/Azure/go-autorest/autorest/date v0.1.0 h1:YGrhWfrgtFs84+h0o46rJrlmsZtyZRg470CqAXTZaGM= -github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= -github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/autorest/mocks v0.2.0 h1:Ww5g4zThfD/6cLb4z6xxgeyDa7QDkizMkJKe0ysZXp0= -github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY= -github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= -github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k= -github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= diff --git a/autorest/to/go_mod_tidy_hack.go b/autorest/to/go_mod_tidy_hack.go index 8e8292107..b7310f6b8 100644 --- a/autorest/to/go_mod_tidy_hack.go +++ b/autorest/to/go_mod_tidy_hack.go @@ -16,9 +16,9 @@ package to // See the License for the specific language governing permissions and // limitations under the License. -// This file, and the github.com/Azure/go-autorest/autorest import, won't actually become part of +// This file, and the github.com/Azure/go-autorest import, won't actually become part of // the resultant binary. // Necessary for safely adding multi-module repo. // See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository -import _ "github.com/Azure/go-autorest/autorest" +import _ "github.com/Azure/go-autorest" diff --git a/autorest/validation/go.mod b/autorest/validation/go.mod index b3f9b6a09..a0a69e9ae 100644 --- a/autorest/validation/go.mod +++ b/autorest/validation/go.mod @@ -3,6 +3,6 @@ module github.com/Azure/go-autorest/autorest/validation go 1.12 require ( - github.com/Azure/go-autorest/autorest v0.9.0 + github.com/Azure/go-autorest v14.2.0+incompatible github.com/stretchr/testify v1.3.0 ) diff --git a/autorest/validation/go.sum b/autorest/validation/go.sum index 6b9010a73..6c1119aab 100644 --- a/autorest/validation/go.sum +++ b/autorest/validation/go.sum @@ -1,24 +1,9 @@ -github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs= -github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= -github.com/Azure/go-autorest/autorest/adal v0.5.0 h1:q2gDruN08/guU9vAjuPWff0+QIrpH6ediguzdAzXAUU= -github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= -github.com/Azure/go-autorest/autorest/date v0.1.0 h1:YGrhWfrgtFs84+h0o46rJrlmsZtyZRg470CqAXTZaGM= -github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= -github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/autorest/mocks v0.2.0 h1:Ww5g4zThfD/6cLb4z6xxgeyDa7QDkizMkJKe0ysZXp0= -github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY= -github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= -github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k= -github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/autorest/validation/go_mod_tidy_hack.go b/autorest/validation/go_mod_tidy_hack.go index 2b2668581..cf1436291 100644 --- a/autorest/validation/go_mod_tidy_hack.go +++ b/autorest/validation/go_mod_tidy_hack.go @@ -16,9 +16,9 @@ package validation // See the License for the specific language governing permissions and // limitations under the License. -// This file, and the github.com/Azure/go-autorest/autorest import, won't actually become part of +// This file, and the github.com/Azure/go-autorest import, won't actually become part of // the resultant binary. // Necessary for safely adding multi-module repo. // See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository -import _ "github.com/Azure/go-autorest/autorest" +import _ "github.com/Azure/go-autorest" diff --git a/logger/go.mod b/logger/go.mod index f22ed56bc..bedeaee03 100644 --- a/logger/go.mod +++ b/logger/go.mod @@ -1,3 +1,5 @@ module github.com/Azure/go-autorest/logger go 1.12 + +require github.com/Azure/go-autorest v14.2.0+incompatible diff --git a/logger/go.sum b/logger/go.sum new file mode 100644 index 000000000..1fc56a962 --- /dev/null +++ b/logger/go.sum @@ -0,0 +1,2 @@ +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= diff --git a/logger/go_mod_tidy_hack.go b/logger/go_mod_tidy_hack.go new file mode 100644 index 000000000..0aa27680d --- /dev/null +++ b/logger/go_mod_tidy_hack.go @@ -0,0 +1,24 @@ +// +build modhack + +package logger + +// 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. + +// This file, and the github.com/Azure/go-autorest import, won't actually become part of +// the resultant binary. + +// Necessary for safely adding multi-module repo. +// See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository +import _ "github.com/Azure/go-autorest" diff --git a/tracing/go.mod b/tracing/go.mod index 25c34c108..a2cdec78c 100644 --- a/tracing/go.mod +++ b/tracing/go.mod @@ -1,3 +1,5 @@ module github.com/Azure/go-autorest/tracing go 1.12 + +require github.com/Azure/go-autorest v14.2.0+incompatible diff --git a/tracing/go.sum b/tracing/go.sum new file mode 100644 index 000000000..1fc56a962 --- /dev/null +++ b/tracing/go.sum @@ -0,0 +1,2 @@ +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= diff --git a/tracing/go_mod_tidy_hack.go b/tracing/go_mod_tidy_hack.go new file mode 100644 index 000000000..e163975cd --- /dev/null +++ b/tracing/go_mod_tidy_hack.go @@ -0,0 +1,24 @@ +// +build modhack + +package tracing + +// 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. + +// This file, and the github.com/Azure/go-autorest import, won't actually become part of +// the resultant binary. + +// Necessary for safely adding multi-module repo. +// See: https://github.com/golang/go/wiki/Modules#is-it-possible-to-add-a-module-to-a-multi-module-repository +import _ "github.com/Azure/go-autorest" From 6badcf928a44de67698f779853bf8611e9994529 Mon Sep 17 00:00:00 2001 From: Panic Stevenson Date: Tue, 23 Jun 2020 11:28:21 -0700 Subject: [PATCH 28/29] Update resourceManagerVMDNSSuffix for AzureUSGovernmentCloud (#531) --- autorest/azure/environments.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autorest/azure/environments.go b/autorest/azure/environments.go index 85ed9635f..72e54ddef 100644 --- a/autorest/azure/environments.go +++ b/autorest/azure/environments.go @@ -127,7 +127,7 @@ var ( KeyVaultDNSSuffix: "vault.usgovcloudapi.net", ServiceBusEndpointSuffix: "servicebus.usgovcloudapi.net", ServiceManagementVMDNSSuffix: "usgovcloudapp.net", - ResourceManagerVMDNSSuffix: "cloudapp.windowsazure.us", + ResourceManagerVMDNSSuffix: "cloudapp.usgovcloudapi.net", ContainerRegistryDNSSuffix: "azurecr.us", CosmosDBDNSSuffix: "documents.azure.us", TokenAudience: "https://management.usgovcloudapi.net/", From 5bb054feebb9126d31711ffd0f00c5102d13cef5 Mon Sep 17 00:00:00 2001 From: Mauro Giusti Date: Tue, 23 Jun 2020 11:28:33 -0700 Subject: [PATCH 29/29] This endpoint changed in AzureChinaCloud (#530) See from Azurre China portal - this is now cloudapp.chinacloudapi.cn --- autorest/azure/environments.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autorest/azure/environments.go b/autorest/azure/environments.go index 72e54ddef..faff93275 100644 --- a/autorest/azure/environments.go +++ b/autorest/azure/environments.go @@ -160,7 +160,7 @@ var ( KeyVaultDNSSuffix: "vault.azure.cn", ServiceBusEndpointSuffix: "servicebus.chinacloudapi.cn", ServiceManagementVMDNSSuffix: "chinacloudapp.cn", - ResourceManagerVMDNSSuffix: "cloudapp.azure.cn", + ResourceManagerVMDNSSuffix: "cloudapp.chinacloudapi.cn", ContainerRegistryDNSSuffix: "azurecr.cn", CosmosDBDNSSuffix: "documents.azure.cn", TokenAudience: "https://management.chinacloudapi.cn/",