pax_global_header00006660000000000000000000000064140415013450014507gustar00rootroot0000000000000052 comment=1c330f69148d5abd7d2097ec6e22cca7c5496883 go-azure-helpers-0.16.0/000077500000000000000000000000001404150134500147645ustar00rootroot00000000000000go-azure-helpers-0.16.0/.gitignore000066400000000000000000000000171404150134500167520ustar00rootroot00000000000000.idea/ vendor/ go-azure-helpers-0.16.0/.travis.yml000066400000000000000000000001701404150134500170730ustar00rootroot00000000000000sudo: false language: go go: - "1.14.x" env: - GO111MODULE=on branches: only: - master script: make test go-azure-helpers-0.16.0/LICENSE000066400000000000000000000370611404150134500160000ustar00rootroot00000000000000Mozilla Public License, version 2.0 1. Definitions 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means a. that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or b. that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: a. any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or b. any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: a. under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and b. under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: a. for any code that a Contributor has removed from Covered Software; or b. for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or c. under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: a. such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and b. You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. 6. Disclaimer of Warranty Covered Software is provided under this License on an "as is" basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer. 7. Limitation of Liability Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party's negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. 8. Litigation Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. go-azure-helpers-0.16.0/Makefile000066400000000000000000000002131404150134500164200ustar00rootroot00000000000000GO111MODULE=on default: test dependencies: go mod download test: dependencies go vet ./... go test -race ./... .PHONY: default test go-azure-helpers-0.16.0/README.md000066400000000000000000000002461404150134500162450ustar00rootroot00000000000000## Azure Helpers This repository contains various helpers and wrappers for working with Azure and [the Azure SDK for Go](https://github.com/Azure/azure-sdk-for-go). go-azure-helpers-0.16.0/authentication/000077500000000000000000000000001404150134500200035ustar00rootroot00000000000000go-azure-helpers-0.16.0/authentication/auth_method.go000066400000000000000000000005551404150134500226400ustar00rootroot00000000000000package authentication import ( "github.com/Azure/go-autorest/autorest" ) type authMethod interface { build(b Builder) (authMethod, error) isApplicable(b Builder) bool getAuthorizationToken(sender autorest.Sender, oauthConfig *OAuthConfig, endpoint string) (autorest.Authorizer, error) name() string populateConfig(c *Config) error validate() error } go-azure-helpers-0.16.0/authentication/auth_method_azure_cli_token.go000066400000000000000000000257101404150134500260750ustar00rootroot00000000000000package authentication import ( "bytes" "context" "encoding/json" "fmt" "os/exec" "strings" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/adal" "github.com/Azure/go-autorest/autorest/azure/cli" "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-version" ) type azureCLIProfile struct { // CLI "subscriptions" are really "accounts" that can represent either a subscription (with tenant) or _just_ a tenant account *cli.Subscription clientId string environment string subscriptionId string tenantId string tenantOnly bool } type azureCliTokenAuth struct { profile *azureCLIProfile servicePrincipalAuthDocsLink string } func (a azureCliTokenAuth) build(b Builder) (authMethod, error) { auth := azureCliTokenAuth{ profile: &azureCLIProfile{ subscriptionId: b.SubscriptionID, tenantId: b.TenantID, tenantOnly: b.TenantOnly, clientId: "04b07795-8ddb-461a-bbee-02f9e1bf7b46", // fixed first party client id for Az CLI }, servicePrincipalAuthDocsLink: b.ClientSecretDocsLink, } if err := auth.checkAzVersion(); err != nil { return nil, err } var acc *cli.Subscription if auth.profile.tenantOnly { var err error acc, err = obtainTenant(b.TenantID) if err != nil { return nil, fmt.Errorf("obtain tenant(%s) from Azure CLI: %+v", b.TenantID, err) } auth.profile.account = acc } else { var err error acc, err = obtainSubscription(b.SubscriptionID) if err != nil { return nil, fmt.Errorf("obtain subscription(%s) from Azure CLI: %+v", b.SubscriptionID, err) } auth.profile.account = acc } // Authenticating as a Service Principal doesn't return all of the information we need for authentication purposes // as such Service Principal authentication is supported using the specific auth method if acc.User == nil || !strings.EqualFold(acc.User.Type, "user") { return nil, fmt.Errorf(`Authenticating using the Azure CLI is only supported as a User (not a Service Principal). To authenticate to Azure using a Service Principal, you can use the separate 'Authenticate using a Service Principal' auth method - instructions for which can be found here: %s Alternatively you can authenticate using the Azure CLI by using a User Account.`, auth.servicePrincipalAuthDocsLink) } // Populate fields if !b.TenantOnly && auth.profile.subscriptionId == "" { auth.profile.subscriptionId = acc.ID } if auth.profile.tenantId == "" { auth.profile.tenantId = acc.TenantID } // always pull the environment from the Azure CLI, since the Access Token's associated with it auth.profile.environment = normalizeEnvironmentName(acc.EnvironmentName) return auth, nil } func (a azureCliTokenAuth) isApplicable(b Builder) bool { return b.SupportsAzureCliToken } func (a azureCliTokenAuth) getAuthorizationToken(sender autorest.Sender, oauth *OAuthConfig, endpoint string) (autorest.Authorizer, error) { if oauth.OAuth == nil { return nil, fmt.Errorf("Error getting Authorization Token for cli auth: an OAuth token wasn't configured correctly; please file a bug with more details") } // the Azure CLI appears to cache these, so to maintain compatibility with the interface this method is intentionally not on the pointer var token *cli.Token var err error if a.profile.tenantOnly { token, err = obtainAuthorizationToken(endpoint, "", a.profile.tenantId) } else { token, err = obtainAuthorizationToken(endpoint, a.profile.subscriptionId, "") } if err != nil { return nil, fmt.Errorf("Error obtaining Authorization Token from the Azure CLI: %s", err) } adalToken, err := token.ToADALToken() if err != nil { return nil, fmt.Errorf("Error converting Authorization Token to an ADAL Token: %s", err) } spt, err := adal.NewServicePrincipalTokenFromManualToken(*oauth.OAuth, a.profile.clientId, endpoint, adalToken) if err != nil { return nil, err } var refreshFunc adal.TokenRefresh = func(ctx context.Context, resource string) (*adal.Token, error) { var token *cli.Token var err error if a.profile.tenantOnly { token, err = obtainAuthorizationToken(resource, "", a.profile.tenantId) } else { token, err = obtainAuthorizationToken(resource, a.profile.subscriptionId, "") } if err != nil { return nil, err } adalToken, err := token.ToADALToken() if err != nil { return nil, err } return &adalToken, nil } spt.SetCustomRefreshFunc(refreshFunc) auth := autorest.NewBearerAuthorizer(spt) return auth, nil } func (a azureCliTokenAuth) name() string { return "Obtaining a token from the Azure CLI" } func (a azureCliTokenAuth) populateConfig(c *Config) error { c.ClientID = a.profile.clientId c.TenantID = a.profile.tenantId c.Environment = a.profile.environment c.SubscriptionID = a.profile.subscriptionId c.GetAuthenticatedObjectID = func(ctx context.Context) (string, error) { objectId, err := obtainAuthenticatedObjectID() if err != nil { return "", err } return objectId, nil } return nil } func (a azureCliTokenAuth) validate() error { var err *multierror.Error errorMessageFmt := "A %s was not found in your Azure CLI Credentials.\n\nPlease login to the Azure CLI again via `az login`" if a.profile == nil { return fmt.Errorf("Azure CLI Profile is nil - this is an internal error and should be reported.") } if a.profile.clientId == "" { err = multierror.Append(err, fmt.Errorf(errorMessageFmt, "Client ID")) } if !a.profile.tenantOnly && a.profile.subscriptionId == "" { err = multierror.Append(err, fmt.Errorf(errorMessageFmt, "Subscription ID")) } if a.profile.tenantId == "" { err = multierror.Append(err, fmt.Errorf(errorMessageFmt, "Tenant ID")) } return err.ErrorOrNil() } func (a azureCliTokenAuth) checkAzVersion() error { // Azure CLI v2.0.79 is the earliest version to have a `version` command var minimumVersion string if a.profile.tenantOnly { // v2.0.81 introduced the `--tenant` option to the `account get-access-token` subcommand minimumVersion = "2.0.81" } else { minimumVersion = "2.0.79" } var cliVersion *struct { AzureCli *string `json:"azure-cli,omitempty"` AzureCliCore *string `json:"azure-cli-core,omitempty"` AzureCliTelemetry *string `json:"azure-cli-telemetry,omitempty"` Extensions *interface{} `json:"extensions,omitempty"` } err := jsonUnmarshalAzCmd(&cliVersion, "version", "-o=json") if err != nil { return fmt.Errorf("Please ensure you have installed Azure CLI version %s or newer. Error parsing json result from the Azure CLI: %v.", minimumVersion, err) } if cliVersion.AzureCli == nil { return fmt.Errorf("Could not detect Azure CLI version. Please ensure you have installed Azure CLI version %s or newer.", minimumVersion) } actual, err := version.NewVersion(*cliVersion.AzureCli) if err != nil { return fmt.Errorf("Could not parse detected Azure CLI version %q: %+v", *cliVersion.AzureCli, err) } supported, err := version.NewVersion(minimumVersion) if err != nil { return fmt.Errorf("Could not parse supported Azure CLI version: %+v", err) } nextMajor, err := version.NewVersion("3.0.0") if err != nil { return fmt.Errorf("Could not parse next major Azure CLI version: %+v", err) } if nextMajor.LessThanOrEqual(actual) { return fmt.Errorf(`Authenticating using the Azure CLI requires a version older than %[1]s but Terraform detected version %[3]s. Please install v%[2]s or newer (but also older than %[1]s) and ensure the correct version is in your path.`, nextMajor.String(), supported.String(), actual.String()) } if actual.LessThan(supported) { return fmt.Errorf(`Authenticating using the Azure CLI requires version %[1]s but Terraform detected version %[2]s. Please install v%[1]s or greater and ensure the correct version is in your path.`, supported.String(), actual.String()) } return nil } func obtainAuthenticatedObjectID() (string, error) { var json struct { ObjectId string `json:"objectId"` } err := jsonUnmarshalAzCmd(&json, "ad", "signed-in-user", "show", "-o=json") if err != nil { return "", fmt.Errorf("Error parsing json result from the Azure CLI: %v", err) } return json.ObjectId, nil } func obtainAuthorizationToken(endpoint string, subscriptionId string, tenantId string) (*cli.Token, error) { var token cli.Token var err error if tenantId != "" { err = jsonUnmarshalAzCmd(&token, "account", "get-access-token", "--resource", endpoint, "--tenant", tenantId, "-o=json") } else { err = jsonUnmarshalAzCmd(&token, "account", "get-access-token", "--resource", endpoint, "--subscription", subscriptionId, "-o=json") } if err != nil { return nil, fmt.Errorf("Error parsing json result from the Azure CLI: %v", err) } return &token, nil } // obtainSubscription returns a Subscription object of the specified subscriptionId. // If the subscriptionId is empty, it selects the default subscription. func obtainSubscription(subscriptionId string) (*cli.Subscription, error) { var acc cli.Subscription cmd := make([]string, 0) cmd = []string{"account", "show", "-o=json"} if subscriptionId != "" { cmd = append(cmd, "-s", subscriptionId) } err := jsonUnmarshalAzCmd(&acc, cmd...) if err != nil { return nil, fmt.Errorf("Error parsing json result from the Azure CLI: %v", err) } return &acc, nil } // obtainTenant returns a Subscription object having the specified tenantId. // If the tenantId is empty, it selects the default subscription. // This works with `az login --allow-no-subscriptions` func obtainTenant(tenantId string) (*cli.Subscription, error) { var acc cli.Subscription if tenantId == "" { cmd := make([]string, 0) cmd = []string{"account", "show", "-o=json"} err := jsonUnmarshalAzCmd(&acc, cmd...) if err != nil { return nil, fmt.Errorf("Error parsing json result from the Azure CLI: %v", err) } } else { var accs []cli.Subscription cmd := make([]string, 0) cmd = []string{"account", "list", "-o=json"} err := jsonUnmarshalAzCmd(&accs, cmd...) if err != nil { return nil, fmt.Errorf("Error parsing json result from the Azure CLI: %v", err) } for _, a := range accs { if a.TenantID == tenantId { acc = a break } } if acc.TenantID == "" { return nil, fmt.Errorf("Tenant %q was not found", tenantId) } } return &acc, nil } func jsonUnmarshalAzCmd(i interface{}, arg ...string) error { var stderr bytes.Buffer var stdout bytes.Buffer cmd := exec.Command("az", arg...) cmd.Stderr = &stderr cmd.Stdout = &stdout if err := cmd.Start(); err != nil { err := fmt.Errorf("Error launching Azure CLI: %+v", err) if stdErrStr := stderr.String(); stdErrStr != "" { err = fmt.Errorf("%s: %s", err, strings.TrimSpace(stdErrStr)) } return err } if err := cmd.Wait(); err != nil { err := fmt.Errorf("Error waiting for the Azure CLI: %+v", err) if stdErrStr := stderr.String(); stdErrStr != "" { err = fmt.Errorf("%s: %s", err, strings.TrimSpace(stdErrStr)) } return err } if err := json.Unmarshal([]byte(stdout.String()), &i); err != nil { return fmt.Errorf("Error unmarshaling the result of Azure CLI: %v", err) } return nil } go-azure-helpers-0.16.0/authentication/auth_method_azure_cli_token_multi_tenant.go000066400000000000000000000145121404150134500306560ustar00rootroot00000000000000package authentication import ( "context" "fmt" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/adal" "github.com/Azure/go-autorest/autorest/azure/cli" "github.com/hashicorp/go-multierror" ) type azureCliTokenMultiTenantAuth struct { profile *azureCLIProfileMultiTenant servicePrincipalAuthDocsLink string } func (a azureCliTokenMultiTenantAuth) build(b Builder) (authMethod, error) { auth := azureCliTokenMultiTenantAuth{ profile: &azureCLIProfileMultiTenant{ clientId: b.ClientID, environment: b.Environment, subscriptionId: b.SubscriptionID, tenantId: b.TenantID, auxiliaryTenantIDs: b.AuxiliaryTenantIDs, }, servicePrincipalAuthDocsLink: b.ClientSecretDocsLink, } profilePath, err := cli.ProfilePath() if err != nil { return nil, fmt.Errorf("Error loading the Profile Path from the Azure CLI: %+v", err) } profile, err := cli.LoadProfile(profilePath) if err != nil { return nil, fmt.Errorf("Azure CLI Authorization Profile was not found. Please ensure the Azure CLI is installed and then log-in with `az login`.") } auth.profile.profile = profile // Authenticating as a Service Principal doesn't return all of the information we need for authentication purposes // as such Service Principal authentication is supported using the specific auth method if authenticatedAsAUser := auth.profile.verifyAuthenticatedAsAUser(); !authenticatedAsAUser { return nil, fmt.Errorf(`Authenticating using the Azure CLI is only supported as a User (not a Service Principal). To authenticate to Azure using a Service Principal, you can use the separate 'Authenticate using a Service Principal' auth method - instructions for which can be found here: %s Alternatively you can authenticate using the Azure CLI by using a User Account.`, auth.servicePrincipalAuthDocsLink) } err = auth.profile.populateFields() if err != nil { return nil, fmt.Errorf("Error retrieving the Profile from the Azure CLI: %s Please re-authenticate using `az login`.", err) } err = auth.profile.populateClientId() if err != nil { return nil, fmt.Errorf("Error populating Client ID from the Azure CLI: %+v", err) } return auth, nil } func (a azureCliTokenMultiTenantAuth) isApplicable(b Builder) bool { return b.SupportsAzureCliToken && b.SupportsAuxiliaryTenants && (len(b.AuxiliaryTenantIDs) > 0) } func (a azureCliTokenMultiTenantAuth) getAuthorizationToken(sender autorest.Sender, oauth *OAuthConfig, endpoint string) (autorest.Authorizer, error) { if oauth.MultiTenantOauth == nil { return nil, fmt.Errorf("Error getting Authorization Token for cli auth: an MultiTenantOauth token wasn't configured correctly; please file a bug with more details") } m := adal.MultiTenantServicePrincipalToken{ AuxiliaryTokens: make([]*adal.ServicePrincipalToken, len(a.profile.auxiliaryTenantIDs)), } // the Azure CLI appears to cache these, so to maintain compatibility with the interface this method is intentionally not on the pointer primaryToken, err := obtainAuthorizationTokenByTenant(endpoint, a.profile.tenantId) if err != nil { return nil, fmt.Errorf("Error obtaining Authorization Token from the Azure CLI: %s", err) } adalToken, err := primaryToken.ToADALToken() if err != nil { return nil, fmt.Errorf("Error converting Authorization Token to an ADAL Token: %s", err) } spt, err := adal.NewServicePrincipalTokenFromManualToken(*oauth.OAuth, a.profile.clientId, endpoint, adalToken) if err != nil { return nil, err } var refreshFunc adal.TokenRefresh = func(ctx context.Context, resource string) (*adal.Token, error) { token, err := obtainAuthorizationToken(resource, a.profile.subscriptionId, "") if err != nil { return nil, err } adalToken, err := token.ToADALToken() if err != nil { return nil, err } return &adalToken, nil } spt.SetCustomRefreshFunc(refreshFunc) m.PrimaryToken = spt for t := range a.profile.auxiliaryTenantIDs { token, err := obtainAuthorizationTokenByTenant(endpoint, a.profile.auxiliaryTenantIDs[t]) if err != nil { return nil, fmt.Errorf("Error obtaining Authorization Token from the Azure CLI: %s", err) } adalToken, err := token.ToADALToken() if err != nil { return nil, fmt.Errorf("Error converting Authorization Token to an ADAL Token: %s", err) } aux, err := adal.NewServicePrincipalTokenFromManualToken(*oauth.OAuth, a.profile.clientId, endpoint, adalToken) if err != nil { return nil, err } aux.SetCustomRefreshFunc(refreshFunc) m.AuxiliaryTokens[t] = aux } auth := autorest.NewMultiTenantServicePrincipalTokenAuthorizer(&m) return auth, nil } func (a azureCliTokenMultiTenantAuth) name() string { return "Obtaining a Multi-tenant token from the Azure CLI" } func (a azureCliTokenMultiTenantAuth) populateConfig(c *Config) error { c.ClientID = a.profile.clientId c.TenantID = a.profile.tenantId c.Environment = a.profile.environment c.SubscriptionID = a.profile.subscriptionId c.GetAuthenticatedObjectID = func(ctx context.Context) (string, error) { objectId, err := obtainAuthenticatedObjectID() if err != nil { return "", err } return objectId, nil } return nil } func (a azureCliTokenMultiTenantAuth) validate() error { var err *multierror.Error errorMessageFmt := "A %s was not found in your Azure CLI Credentials.\n\nPlease login to the Azure CLI again via `az login`" if a.profile == nil { return fmt.Errorf("Azure CLI Profile is nil - this is an internal error and should be reported.") } if a.profile.clientId == "" { err = multierror.Append(err, fmt.Errorf(errorMessageFmt, "Client ID")) } if a.profile.subscriptionId == "" { err = multierror.Append(err, fmt.Errorf(errorMessageFmt, "Subscription ID")) } if a.profile.tenantId == "" { err = multierror.Append(err, fmt.Errorf(errorMessageFmt, "Tenant ID")) } if len(a.profile.auxiliaryTenantIDs) == 0 { err = multierror.Append(err, fmt.Errorf("Aux Tenant IDs missing from Multi Tenant configuration")) } return err.ErrorOrNil() } func obtainAuthorizationTokenByTenant(endpoint string, tenantId string) (*cli.Token, error) { var token cli.Token err := jsonUnmarshalAzCmd(&token, "account", "get-access-token", "--resource", endpoint, "--tenant", tenantId, "--only-show-errors", "-o=json") if err != nil { return nil, fmt.Errorf("Error parsing json result from the Azure CLI: %v", err) } return &token, nil } go-azure-helpers-0.16.0/authentication/auth_method_azure_cli_token_multi_tenant_test.go000066400000000000000000000115651404150134500317220ustar00rootroot00000000000000package authentication import ( "testing" ) func TestAzureCLITokenMultiTenantAuth_isApplicable(t *testing.T) { cases := []struct { Description string Builder Builder Valid bool }{ { Description: "Empty Configuration", Builder: Builder{}, Valid: false, }, { Description: "Feature Toggled off", Builder: Builder{ SupportsAzureCliToken: false, SupportsAuxiliaryTenants: true, AuxiliaryTenantIDs: []string{"test"}, }, Valid: false, }, { Description: "Aux Tenant Feature Toggled off", Builder: Builder{ SupportsAzureCliToken: true, SupportsAuxiliaryTenants: false, AuxiliaryTenantIDs: []string{"test"}, }, Valid: false, }, { Description: "Empty Aux Tenants", Builder: Builder{ SupportsAzureCliToken: false, SupportsAuxiliaryTenants: true, AuxiliaryTenantIDs: []string{}, }, Valid: false, }, { Description: "Feature Toggled on", Builder: Builder{ SupportsAzureCliToken: true, SupportsAuxiliaryTenants: true, AuxiliaryTenantIDs: []string{"test"}, }, Valid: true, }, } for _, v := range cases { applicable := azureCliTokenMultiTenantAuth{}.isApplicable(v.Builder) if v.Valid != applicable { t.Fatalf("Expected %q to be %t but got %t", v.Description, v.Valid, applicable) } } } func TestAzureCLITokenMultiTenantAuth_populateConfig(t *testing.T) { config := &Config{} auth := azureCliTokenMultiTenantAuth{ profile: &azureCLIProfileMultiTenant{ clientId: "some-subscription-id", environment: "dimension-c137", subscriptionId: "some-subscription-id", tenantId: "some-tenant-id", auxiliaryTenantIDs: []string{"aux-tenant-id"}, }, } err := auth.populateConfig(config) if err != nil { t.Fatalf("Error populating config: %s", err) } if auth.profile.clientId != config.ClientID { t.Fatalf("Expected Client ID to be %q but got %q", auth.profile.tenantId, config.TenantID) } if auth.profile.environment != config.Environment { t.Fatalf("Expected Environment to be %q but got %q", auth.profile.tenantId, config.TenantID) } if auth.profile.subscriptionId != config.SubscriptionID { t.Fatalf("Expected Subscription ID to be %q but got %q", auth.profile.tenantId, config.TenantID) } if auth.profile.tenantId != config.TenantID { t.Fatalf("Expected Tenant ID to be %q but got %q", auth.profile.tenantId, config.TenantID) } } func TestAzureCLITokenMultiTenantAuth_validate(t *testing.T) { cases := []struct { Description string Config azureCliTokenMultiTenantAuth ExpectError bool }{ { Description: "Empty Configuration", Config: azureCliTokenMultiTenantAuth{}, ExpectError: true, }, { Description: "Missing Client ID", Config: azureCliTokenMultiTenantAuth{ profile: &azureCLIProfileMultiTenant{ subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", auxiliaryTenantIDs: []string{"9834f8d0-24b3-41b7-8b8d-000000000000"}, }, }, ExpectError: true, }, { Description: "Missing Subscription ID", Config: azureCliTokenMultiTenantAuth{ profile: &azureCLIProfileMultiTenant{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", auxiliaryTenantIDs: []string{"9834f8d0-24b3-41b7-8b8d-000000000000"}, }, }, ExpectError: true, }, { Description: "Missing Tenant ID", Config: azureCliTokenMultiTenantAuth{ profile: &azureCLIProfileMultiTenant{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", auxiliaryTenantIDs: []string{"9834f8d0-24b3-41b7-8b8d-000000000000"}, }, }, ExpectError: true, }, { Description: "Missing aux tenant IDs", Config: azureCliTokenMultiTenantAuth{ profile: &azureCLIProfileMultiTenant{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", }, }, ExpectError: true, }, { Description: "Valid Configuration", Config: azureCliTokenMultiTenantAuth{ profile: &azureCLIProfileMultiTenant{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", auxiliaryTenantIDs: []string{"9834f8d0-24b3-41b7-8b8d-000000000000"}, }, }, ExpectError: false, }, } for _, v := range cases { err := v.Config.validate() if v.ExpectError && err == nil { t.Fatalf("Expected an error for %q: didn't get one", v.Description) } if !v.ExpectError && err != nil { t.Fatalf("Expected there to be no error for %q - but got: %v", v.Description, err) } } } go-azure-helpers-0.16.0/authentication/auth_method_azure_cli_token_test.go000066400000000000000000000075761404150134500271460ustar00rootroot00000000000000package authentication import ( "testing" ) func TestAzureCLITokenAuth_isApplicable(t *testing.T) { cases := []struct { Description string Builder Builder Valid bool }{ { Description: "Empty Configuration", Builder: Builder{}, Valid: false, }, { Description: "Feature Toggled off", Builder: Builder{ SupportsAzureCliToken: false, }, Valid: false, }, { Description: "Feature Toggled on", Builder: Builder{ SupportsAzureCliToken: true, }, Valid: true, }, } for _, v := range cases { applicable := azureCliTokenAuth{}.isApplicable(v.Builder) if v.Valid != applicable { t.Fatalf("Expected %q to be %t but got %t", v.Description, v.Valid, applicable) } } } func TestAzureCLITokenAuth_populateConfig(t *testing.T) { config := &Config{} auth := azureCliTokenAuth{ profile: &azureCLIProfile{ clientId: "some-subscription-id", environment: "dimension-c137", subscriptionId: "some-subscription-id", tenantId: "some-tenant-id", }, } err := auth.populateConfig(config) if err != nil { t.Fatalf("Error populating config: %s", err) } if auth.profile.clientId != config.ClientID { t.Fatalf("Expected Client ID to be %q but got %q", auth.profile.tenantId, config.TenantID) } if auth.profile.environment != config.Environment { t.Fatalf("Expected Environment to be %q but got %q", auth.profile.tenantId, config.TenantID) } if auth.profile.subscriptionId != config.SubscriptionID { t.Fatalf("Expected Subscription ID to be %q but got %q", auth.profile.tenantId, config.TenantID) } if auth.profile.tenantId != config.TenantID { t.Fatalf("Expected Tenant ID to be %q but got %q", auth.profile.tenantId, config.TenantID) } } func TestAzureCLITokenAuth_validate(t *testing.T) { cases := []struct { Description string Config azureCliTokenAuth ExpectError bool }{ { Description: "Empty Configuration", Config: azureCliTokenAuth{}, ExpectError: true, }, { Description: "Missing Client ID", Config: azureCliTokenAuth{ profile: &azureCLIProfile{ subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", }, }, ExpectError: true, }, { Description: "Missing Subscription ID", Config: azureCliTokenAuth{ profile: &azureCLIProfile{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", }, }, ExpectError: true, }, { Description: "Missing Tenant ID", Config: azureCliTokenAuth{ profile: &azureCLIProfile{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", }, }, ExpectError: true, }, { Description: "Valid Configuration", Config: azureCliTokenAuth{ profile: &azureCLIProfile{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", }, }, ExpectError: false, }, { Description: "Valid TenantOnly Configuration", Config: azureCliTokenAuth{ profile: &azureCLIProfile{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", tenantOnly: true, }, }, ExpectError: false, }, { Description: "Invalid TenantOnly Configuration", Config: azureCliTokenAuth{ profile: &azureCLIProfile{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", tenantOnly: true, }, }, ExpectError: true, }, } for _, v := range cases { err := v.Config.validate() if v.ExpectError && err == nil { t.Fatalf("Expected an error for %q: didn't get one", v.Description) } if !v.ExpectError && err != nil { t.Fatalf("Expected there to be no error for %q - but got: %v", v.Description, err) } } } go-azure-helpers-0.16.0/authentication/auth_method_client_cert.go000066400000000000000000000072171404150134500252150ustar00rootroot00000000000000package authentication import ( "crypto/rsa" "crypto/x509" "fmt" "io/ioutil" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/adal" "github.com/hashicorp/go-multierror" "golang.org/x/crypto/pkcs12" ) type servicePrincipalClientCertificateAuth struct { clientId string clientCertPath string clientCertPassword string subscriptionId string tenantId string tenantOnly bool } func (a servicePrincipalClientCertificateAuth) build(b Builder) (authMethod, error) { method := servicePrincipalClientCertificateAuth{ clientId: b.ClientID, clientCertPath: b.ClientCertPath, clientCertPassword: b.ClientCertPassword, subscriptionId: b.SubscriptionID, tenantId: b.TenantID, tenantOnly: b.TenantOnly, } return method, nil } func (a servicePrincipalClientCertificateAuth) isApplicable(b Builder) bool { return b.SupportsClientCertAuth && b.ClientCertPath != "" } func (a servicePrincipalClientCertificateAuth) name() string { return "Service Principal / Client Certificate" } func (a servicePrincipalClientCertificateAuth) getAuthorizationToken(sender autorest.Sender, oauth *OAuthConfig, endpoint string) (autorest.Authorizer, error) { if oauth.OAuth == nil { return nil, fmt.Errorf("Error getting Authorization Token for client cert: an OAuth token wasn't configured correctly; please file a bug with more details") } // Get the certificate and private key from pfx file certificate, rsaPrivateKey, err := decodePkcs12File(a.clientCertPath, a.clientCertPassword) if err != nil { return nil, fmt.Errorf("Error decoding pkcs12 certificate: %v", err) } spt, err := adal.NewServicePrincipalTokenFromCertificate(*oauth.OAuth, a.clientId, certificate, rsaPrivateKey, endpoint) if err != nil { return nil, err } spt.SetSender(sender) err = spt.Refresh() if err != nil { return nil, err } auth := autorest.NewBearerAuthorizer(spt) return auth, nil } func (a servicePrincipalClientCertificateAuth) populateConfig(c *Config) error { c.AuthenticatedAsAServicePrincipal = true c.GetAuthenticatedObjectID = buildServicePrincipalObjectIDFunc(c) return nil } func (a servicePrincipalClientCertificateAuth) validate() error { var err *multierror.Error fmtErrorMessage := "A %s must be configured when authenticating as a Service Principal using a Client Certificate." if !a.tenantOnly && a.subscriptionId == "" { err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Subscription ID")) } if a.clientId == "" { err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Client ID")) } if a.clientCertPath == "" { err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Client Certificate Path")) } else { // validate the certificate path is a valid pfx file _, _, derr := decodePkcs12File(a.clientCertPath, a.clientCertPassword) if derr != nil { err = multierror.Append(err, fmt.Errorf("The Client Certificate Path is not a valid pfx file: %v", derr)) } } if a.tenantId == "" { err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Tenant ID")) } return err.ErrorOrNil() } func decodePkcs12File(f string, password string) (*x509.Certificate, *rsa.PrivateKey, error) { certificateData, err := ioutil.ReadFile(f) if err != nil { return nil, nil, fmt.Errorf("Error reading Client Certificate %q: %v", f, err) } privateKey, certificate, err := pkcs12.Decode(certificateData, password) if err != nil { return nil, nil, err } rsaPrivateKey, isRsaKey := privateKey.(*rsa.PrivateKey) if !isRsaKey { return nil, nil, fmt.Errorf("PKCS#12 certificate must contain an RSA private key") } return certificate, rsaPrivateKey, nil } go-azure-helpers-0.16.0/authentication/auth_method_client_cert_test.go000066400000000000000000000162401404150134500262500ustar00rootroot00000000000000package authentication import ( "testing" ) func TestServicePrincipalClientCertAuth_builder(t *testing.T) { builder := Builder{ ClientID: "some-client-id", ClientCertPath: "some-client-cert-path", ClientCertPassword: "some-password", Environment: "some-environment", SubscriptionID: "some-subscription-id", TenantID: "some-tenant-id", } config, err := servicePrincipalClientCertificateAuth{}.build(builder) if err != nil { t.Fatalf("Error building client cert auth: %s", err) } servicePrincipal := config.(servicePrincipalClientCertificateAuth) if builder.ClientID != servicePrincipal.clientId { t.Fatalf("Expected Client ID to be %q but got %q", builder.ClientID, servicePrincipal.clientId) } if builder.ClientCertPath != servicePrincipal.clientCertPath { t.Fatalf("Expected Client Certificate Path to be %q but got %q", builder.ClientCertPath, servicePrincipal.clientCertPath) } if builder.ClientCertPassword != servicePrincipal.clientCertPassword { t.Fatalf("Expected Client Certificate Password to be %q but got %q", builder.ClientCertPassword, servicePrincipal.clientCertPassword) } if builder.SubscriptionID != servicePrincipal.subscriptionId { t.Fatalf("Expected Subscription ID to be %q but got %q", builder.SubscriptionID, servicePrincipal.subscriptionId) } if builder.TenantID != servicePrincipal.tenantId { t.Fatalf("Expected Tenant ID to be %q but got %q", builder.TenantID, servicePrincipal.tenantId) } } func TestServicePrincipalClientCertAuth_isApplicable(t *testing.T) { cases := []struct { Description string Builder Builder Valid bool }{ { Description: "Empty Configuration", Builder: Builder{}, Valid: false, }, { Description: "Feature Toggled off", Builder: Builder{ SupportsClientCertAuth: false, }, Valid: false, }, { Description: "Feature Toggled on but no cert specified", Builder: Builder{ SupportsClientCertAuth: true, }, Valid: false, }, { Description: "Cert specified but feature toggled off", Builder: Builder{ ClientCertPath: "./path/to/file", }, Valid: false, }, { Description: "Valid configuration", Builder: Builder{ SupportsClientCertAuth: true, ClientCertPath: "./path/to/file", }, Valid: true, }, } for _, v := range cases { applicable := servicePrincipalClientCertificateAuth{}.isApplicable(v.Builder) if v.Valid != applicable { t.Fatalf("Expected %q to be %t but got %t", v.Description, v.Valid, applicable) } } } func TestServicePrincipalClientCertAuth_populateConfig(t *testing.T) { config := &Config{} err := servicePrincipalClientCertificateAuth{}.populateConfig(config) if err != nil { t.Fatalf("Error populating config: %s", err) } if !config.AuthenticatedAsAServicePrincipal { t.Fatalf("Expected `AuthenticatedAsAServicePrincipal` to be true but it wasn't") } } func TestServicePrincipalClientCertAuth_validate(t *testing.T) { filePath := "./testdata/sp.pfx" emptyPath := "./testdata/empty" aliasFilePath := "./testdata/sp.pfx.alias" cases := []struct { Description string Config servicePrincipalClientCertificateAuth ExpectError bool }{ { Description: "Empty Configuration", Config: servicePrincipalClientCertificateAuth{}, ExpectError: true, }, { Description: "Missing Client ID", Config: servicePrincipalClientCertificateAuth{ subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", clientCertPath: filePath, tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", }, ExpectError: true, }, { Description: "Missing Subscription ID", Config: servicePrincipalClientCertificateAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", clientCertPath: filePath, tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", }, ExpectError: true, }, { Description: "Missing Client Certificate Path", Config: servicePrincipalClientCertificateAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", }, ExpectError: true, }, { Description: "Missing Tenant ID", Config: servicePrincipalClientCertificateAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", clientCertPath: filePath, }, ExpectError: true, }, { Description: "File isn't an valid pfx", Config: servicePrincipalClientCertificateAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", clientCertPath: emptyPath, tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", }, ExpectError: true, }, { Description: "File does not exist", Config: servicePrincipalClientCertificateAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", clientCertPath: "does-not-exist.pfx", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", }, ExpectError: true, }, { Description: "Valid Configuration but incorrect password", Config: servicePrincipalClientCertificateAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", clientCertPath: filePath, tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", }, ExpectError: true, }, { Description: "Valid Configuration", Config: servicePrincipalClientCertificateAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", clientCertPath: filePath, clientCertPassword: "123", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", }, ExpectError: false, }, { Description: "Valid Configuration with file not end with .pfx", Config: servicePrincipalClientCertificateAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", clientCertPath: aliasFilePath, clientCertPassword: "123", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", }, ExpectError: false, }, { Description: "Invalid TenantOnly Configuration", Config: servicePrincipalClientCertificateAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", clientCertPath: filePath, clientCertPassword: "123", tenantOnly: true, }, ExpectError: true, }, { Description: "Valid TenantOnly Configuration", Config: servicePrincipalClientCertificateAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", clientCertPath: filePath, clientCertPassword: "123", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", tenantOnly: true, }, ExpectError: false, }, } for _, v := range cases { err := v.Config.validate() if v.ExpectError && err == nil { t.Fatalf("Expected an error for %q: didn't get one", v.Description) } if !v.ExpectError && err != nil { t.Fatalf("Expected there to be no error for %q - but got: %v", v.Description, err) } } } go-azure-helpers-0.16.0/authentication/auth_method_client_secret.go000066400000000000000000000044041404150134500255400ustar00rootroot00000000000000package authentication import ( "fmt" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/adal" "github.com/hashicorp/go-multierror" ) type servicePrincipalClientSecretAuth struct { clientId string clientSecret string subscriptionId string tenantId string tenantOnly bool } func (a servicePrincipalClientSecretAuth) build(b Builder) (authMethod, error) { method := servicePrincipalClientSecretAuth{ clientId: b.ClientID, clientSecret: b.ClientSecret, subscriptionId: b.SubscriptionID, tenantId: b.TenantID, tenantOnly: b.TenantOnly, } return method, nil } func (a servicePrincipalClientSecretAuth) isApplicable(b Builder) bool { return b.SupportsClientSecretAuth && b.ClientSecret != "" } func (a servicePrincipalClientSecretAuth) name() string { return "Service Principal / Client Secret" } func (a servicePrincipalClientSecretAuth) getAuthorizationToken(sender autorest.Sender, oauth *OAuthConfig, endpoint string) (autorest.Authorizer, error) { if oauth.OAuth == nil { return nil, fmt.Errorf("Error getting Authorization Token for client secret auth: an OAuth token wasn't configured correctly; please file a bug with more details") } spt, err := adal.NewServicePrincipalToken(*oauth.OAuth, a.clientId, a.clientSecret, endpoint) if err != nil { return nil, err } spt.SetSender(sender) return autorest.NewBearerAuthorizer(spt), nil } func (a servicePrincipalClientSecretAuth) populateConfig(c *Config) error { c.AuthenticatedAsAServicePrincipal = true c.GetAuthenticatedObjectID = buildServicePrincipalObjectIDFunc(c) return nil } func (a servicePrincipalClientSecretAuth) validate() error { var err *multierror.Error fmtErrorMessage := "A %s must be configured when authenticating as a Service Principal using a Client Secret." if !a.tenantOnly && a.subscriptionId == "" { err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Subscription ID")) } if a.clientId == "" { err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Client ID")) } if a.clientSecret == "" { err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Client Secret")) } if a.tenantId == "" { err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Tenant ID")) } return err.ErrorOrNil() } go-azure-helpers-0.16.0/authentication/auth_method_client_secret_multi_tenant.go000066400000000000000000000055001404150134500303210ustar00rootroot00000000000000package authentication import ( "fmt" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/adal" "github.com/hashicorp/go-multierror" ) type servicePrincipalClientSecretMultiTenantAuth struct { clientId string clientSecret string subscriptionId string tenantId string tenantOnly bool auxiliaryTenantIDs []string } func (a servicePrincipalClientSecretMultiTenantAuth) build(b Builder) (authMethod, error) { method := servicePrincipalClientSecretMultiTenantAuth{ clientId: b.ClientID, clientSecret: b.ClientSecret, subscriptionId: b.SubscriptionID, tenantId: b.TenantID, tenantOnly: b.TenantOnly, auxiliaryTenantIDs: b.AuxiliaryTenantIDs, } return method, nil } func (a servicePrincipalClientSecretMultiTenantAuth) isApplicable(b Builder) bool { return b.SupportsClientSecretAuth && b.ClientSecret != "" && b.SupportsAuxiliaryTenants && (len(b.AuxiliaryTenantIDs) > 0) } func (a servicePrincipalClientSecretMultiTenantAuth) name() string { return "Multi Tenant Service Principal / Client Secret" } func (a servicePrincipalClientSecretMultiTenantAuth) getAuthorizationToken(sender autorest.Sender, oauth *OAuthConfig, endpoint string) (autorest.Authorizer, error) { if oauth.MultiTenantOauth == nil { return nil, fmt.Errorf("Error getting Authorization Token for client cert: an MultiTenantOauth token wasn't configured correctly; please file a bug with more details") } spt, err := adal.NewMultiTenantServicePrincipalToken(*oauth.MultiTenantOauth, a.clientId, a.clientSecret, endpoint) if err != nil { return nil, err } spt.PrimaryToken.SetSender(sender) for _, t := range spt.AuxiliaryTokens { t.SetSender(sender) } auth := autorest.NewMultiTenantServicePrincipalTokenAuthorizer(spt) return auth, nil } func (a servicePrincipalClientSecretMultiTenantAuth) populateConfig(c *Config) error { c.AuthenticatedAsAServicePrincipal = true c.GetAuthenticatedObjectID = buildServicePrincipalObjectIDFunc(c) return nil } func (a servicePrincipalClientSecretMultiTenantAuth) validate() error { var err *multierror.Error fmtErrorMessage := "A %s must be configured when authenticating as a Service Principal using a Multi Tenant Client Secret." if !a.tenantOnly && a.subscriptionId == "" { err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Subscription ID")) } if a.clientId == "" { err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Client ID")) } if a.clientSecret == "" { err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Client Secret")) } if a.tenantId == "" { err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Tenant ID")) } if len(a.auxiliaryTenantIDs) == 0 { err = multierror.Append(err, fmt.Errorf(fmtErrorMessage, "Auxiliary Tenant IDs")) } return err.ErrorOrNil() } go-azure-helpers-0.16.0/authentication/auth_method_client_secret_multi_tenant_test.go000066400000000000000000000167241404150134500313720ustar00rootroot00000000000000package authentication import "testing" func TestServicePrincipalClientSecretMultiTenantAuth_builder(t *testing.T) { builder := Builder{ ClientID: "some-client-id", ClientSecret: "some-client-secret", SubscriptionID: "some-subscription-id", TenantID: "some-tenant-id", AuxiliaryTenantIDs: []string{"aux-tenant-id1", "aux-tenant-id2"}, } config, err := servicePrincipalClientSecretMultiTenantAuth{}.build(builder) if err != nil { t.Fatalf("Error building client secret auth: %s", err) } servicePrincipal := config.(servicePrincipalClientSecretMultiTenantAuth) if builder.ClientID != servicePrincipal.clientId { t.Fatalf("Expected Client ID to be %q but got %q", builder.ClientID, servicePrincipal.clientId) } if builder.ClientSecret != servicePrincipal.clientSecret { t.Fatalf("Expected Client Secret to be %q but got %q", builder.ClientSecret, servicePrincipal.clientSecret) } if builder.SubscriptionID != servicePrincipal.subscriptionId { t.Fatalf("Expected Subscription ID to be %q but got %q", builder.SubscriptionID, servicePrincipal.subscriptionId) } if builder.TenantID != servicePrincipal.tenantId { t.Fatalf("Expected Tenant ID to be %q but got %q", builder.TenantID, servicePrincipal.tenantId) } if builder.AuxiliaryTenantIDs[0] != servicePrincipal.auxiliaryTenantIDs[0] { t.Fatalf("Expected Auxiliary Tenant ID 1 to be %q but got %q", builder.TenantID[0], servicePrincipal.tenantId[0]) } if builder.AuxiliaryTenantIDs[1] != servicePrincipal.auxiliaryTenantIDs[1] { t.Fatalf("Expected Auxiliary Tenant ID 2 to be %q but got %q", builder.TenantID[1], servicePrincipal.tenantId[1]) } if len(builder.AuxiliaryTenantIDs) != len(servicePrincipal.auxiliaryTenantIDs) { t.Fatalf("Expected len(Auxiliary Tenant ID) to be %q but got %q", len(builder.TenantID), len(servicePrincipal.tenantId)) } } func TestServicePrincipalClientSecretMultiTenantAuth_isApplicable(t *testing.T) { cases := []struct { Description string Builder Builder Valid bool }{ { Description: "Empty Configuration", Builder: Builder{}, Valid: false, }, { Description: "Feature Toggled off", Builder: Builder{ SupportsClientSecretAuth: false, }, Valid: false, }, { Description: "Feature Toggled on but no secret specified", Builder: Builder{ SupportsClientSecretAuth: true, }, Valid: false, }, { Description: "Secret specified but feature toggled off", Builder: Builder{ ClientSecret: "I turned myself into a pickle morty!", }, Valid: false, }, { Description: "Multi Tenant not enabled", Builder: Builder{ SupportsClientSecretAuth: true, ClientSecret: "I turned myself into a pickle morty!", }, Valid: false, }, { Description: "Missing Auxiliary Tenants", Builder: Builder{ SupportsClientSecretAuth: true, SupportsAuxiliaryTenants: true, ClientSecret: "I turned myself into a pickle morty!", }, Valid: false, }, { Description: "Valid configuration", Builder: Builder{ SupportsClientSecretAuth: true, SupportsAuxiliaryTenants: true, AuxiliaryTenantIDs: []string{"aux-tenant-id1", "aux-tenant-id2"}, ClientSecret: "I turned myself into a pickle morty!", }, Valid: true, }, } for _, v := range cases { applicable := servicePrincipalClientSecretMultiTenantAuth{}.isApplicable(v.Builder) if v.Valid != applicable { t.Fatalf("Expected %q to be %t but got %t", v.Description, v.Valid, applicable) } } } func TestServicePrincipalClientSecretMultiTenantAuth_populateConfig(t *testing.T) { config := &Config{} err := servicePrincipalClientSecretMultiTenantAuth{}.populateConfig(config) if err != nil { t.Fatalf("Error populating config: %s", err) } if !config.AuthenticatedAsAServicePrincipal { t.Fatalf("Expected `AuthenticatedAsAServicePrincipal` to be true but it wasn't") } } func TestServicePrincipalClientSecretMultiTenantAuth_validate(t *testing.T) { cases := []struct { Description string Config servicePrincipalClientSecretMultiTenantAuth ExpectError bool }{ { Description: "Empty Configuration", Config: servicePrincipalClientSecretMultiTenantAuth{}, ExpectError: true, }, { Description: "Missing Client ID", Config: servicePrincipalClientSecretMultiTenantAuth{ subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", clientSecret: "Does Hammer Time have Daylight Savings Time?", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", }, ExpectError: true, }, { Description: "Missing Subscription ID", Config: servicePrincipalClientSecretMultiTenantAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", clientSecret: "Does Hammer Time have Daylight Savings Time?", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", }, ExpectError: true, }, { Description: "Missing Client Secret", Config: servicePrincipalClientSecretMultiTenantAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", }, ExpectError: true, }, { Description: "Missing Tenant ID", Config: servicePrincipalClientSecretMultiTenantAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", clientSecret: "Does Hammer Time have Daylight Savings Time?", }, ExpectError: true, }, { Description: "Missing Auxiliary Tenants ID", Config: servicePrincipalClientSecretMultiTenantAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", clientSecret: "Does Hammer Time have Daylight Savings Time?", }, ExpectError: true, }, { Description: "Valid Configuration", Config: servicePrincipalClientSecretMultiTenantAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", clientSecret: "Does Hammer Time have Daylight Savings Time?", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", auxiliaryTenantIDs: []string{"9834f8d0-0707-1984-bd35-c611c461a129", "9834f8d0-1984-0707-bd35-c611c461a129"}, }, ExpectError: false, }, { Description: "Invalid TenantOnly Configuration", Config: servicePrincipalClientSecretMultiTenantAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", clientSecret: "Does Hammer Time have Daylight Savings Time?", tenantOnly: true, auxiliaryTenantIDs: []string{"9834f8d0-0707-1984-bd35-c611c461a129", "9834f8d0-1984-0707-bd35-c611c461a129"}, }, ExpectError: true, }, { Description: "Valid TenantOnly Configuration", Config: servicePrincipalClientSecretMultiTenantAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", clientSecret: "Does Hammer Time have Daylight Savings Time?", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", tenantOnly: true, auxiliaryTenantIDs: []string{"9834f8d0-0707-1984-bd35-c611c461a129", "9834f8d0-1984-0707-bd35-c611c461a129"}, }, ExpectError: false, }, } for _, v := range cases { err := v.Config.validate() if v.ExpectError && err == nil { t.Fatalf("Expected an error for %q: didn't get one", v.Description) } if !v.ExpectError && err != nil { t.Fatalf("Expected there to be no error for %q - but got: %v", v.Description, err) } } } go-azure-helpers-0.16.0/authentication/auth_method_client_secret_test.go000066400000000000000000000125621404150134500266030ustar00rootroot00000000000000package authentication import "testing" func TestServicePrincipalClientSecretAuth_builder(t *testing.T) { builder := Builder{ ClientID: "some-client-id", ClientSecret: "some-client-secret", SubscriptionID: "some-subscription-id", TenantID: "some-tenant-id", } config, err := servicePrincipalClientSecretAuth{}.build(builder) if err != nil { t.Fatalf("Error building client secret auth: %s", err) } servicePrincipal := config.(servicePrincipalClientSecretAuth) if builder.ClientID != servicePrincipal.clientId { t.Fatalf("Expected Client ID to be %q but got %q", builder.ClientID, servicePrincipal.clientId) } if builder.ClientSecret != servicePrincipal.clientSecret { t.Fatalf("Expected Client Secret to be %q but got %q", builder.ClientSecret, servicePrincipal.clientSecret) } if builder.SubscriptionID != servicePrincipal.subscriptionId { t.Fatalf("Expected Subscription ID to be %q but got %q", builder.SubscriptionID, servicePrincipal.subscriptionId) } if builder.TenantID != servicePrincipal.tenantId { t.Fatalf("Expected Tenant ID to be %q but got %q", builder.TenantID, servicePrincipal.tenantId) } } func TestServicePrincipalClientSecretAuth_isApplicable(t *testing.T) { cases := []struct { Description string Builder Builder Valid bool }{ { Description: "Empty Configuration", Builder: Builder{}, Valid: false, }, { Description: "Feature Toggled off", Builder: Builder{ SupportsClientSecretAuth: false, }, Valid: false, }, { Description: "Feature Toggled on but no secret specified", Builder: Builder{ SupportsClientSecretAuth: true, }, Valid: false, }, { Description: "Secret specified but feature toggled off", Builder: Builder{ ClientSecret: "I turned myself into a pickle morty!", }, Valid: false, }, { Description: "Valid configuration", Builder: Builder{ SupportsClientSecretAuth: true, ClientSecret: "I turned myself into a pickle morty!", }, Valid: true, }, } for _, v := range cases { applicable := servicePrincipalClientSecretAuth{}.isApplicable(v.Builder) if v.Valid != applicable { t.Fatalf("Expected %q to be %t but got %t", v.Description, v.Valid, applicable) } } } func TestServicePrincipalClientSecretAuth_populateConfig(t *testing.T) { config := &Config{} err := servicePrincipalClientSecretAuth{}.populateConfig(config) if err != nil { t.Fatalf("Error populating config: %s", err) } if !config.AuthenticatedAsAServicePrincipal { t.Fatalf("Expected `AuthenticatedAsAServicePrincipal` to be true but it wasn't") } } func TestServicePrincipalClientSecretAuth_validate(t *testing.T) { cases := []struct { Description string Config servicePrincipalClientSecretAuth ExpectError bool }{ { Description: "Empty Configuration", Config: servicePrincipalClientSecretAuth{}, ExpectError: true, }, { Description: "Missing Client ID", Config: servicePrincipalClientSecretAuth{ subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", clientSecret: "Does Hammer Time have Daylight Savings Time?", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", }, ExpectError: true, }, { Description: "Missing Subscription ID", Config: servicePrincipalClientSecretAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", clientSecret: "Does Hammer Time have Daylight Savings Time?", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", }, ExpectError: true, }, { Description: "Missing Client Secret", Config: servicePrincipalClientSecretAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", }, ExpectError: true, }, { Description: "Missing Tenant ID", Config: servicePrincipalClientSecretAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", clientSecret: "Does Hammer Time have Daylight Savings Time?", }, ExpectError: true, }, { Description: "Valid Configuration", Config: servicePrincipalClientSecretAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", subscriptionId: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2", clientSecret: "Does Hammer Time have Daylight Savings Time?", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", }, ExpectError: false, }, { Description: "Invalid TenantOnly Configuration", Config: servicePrincipalClientSecretAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", clientSecret: "Does Hammer Time have Daylight Savings Time?", tenantOnly: true, }, ExpectError: true, }, { Description: "Valid TenantOnly Configuration", Config: servicePrincipalClientSecretAuth{ clientId: "62e73395-5017-43b6-8ebf-d6c30a514cf1", clientSecret: "Does Hammer Time have Daylight Savings Time?", tenantId: "9834f8d0-24b3-41b7-8b8d-c611c461a129", tenantOnly: true, }, ExpectError: false, }, } for _, v := range cases { err := v.Config.validate() if v.ExpectError && err == nil { t.Fatalf("Expected an error for %q: didn't get one", v.Description) } if !v.ExpectError && err != nil { t.Fatalf("Expected there to be no error for %q - but got: %v", v.Description, err) } } } go-azure-helpers-0.16.0/authentication/auth_method_msi.go000066400000000000000000000052241404150134500235060ustar00rootroot00000000000000package authentication import ( "fmt" "log" "os" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/adal" "github.com/hashicorp/go-multierror" ) type managedServiceIdentityAuth struct { msiEndpoint string clientID string } func (a managedServiceIdentityAuth) build(b Builder) (authMethod, error) { msiEndpoint := b.MsiEndpoint if msiEndpoint == "" { //nolint:SA1019 ep, err := adal.GetMSIVMEndpoint() if err != nil { return nil, fmt.Errorf("determining MSI Endpoint: ensure the VM has MSI enabled, or configure the MSI Endpoint. Error: %s", err) } msiEndpoint = ep } log.Printf("[DEBUG] Using MSI msiEndpoint %q", msiEndpoint) auth := managedServiceIdentityAuth{ msiEndpoint: msiEndpoint, clientID: b.ClientID, } return auth, nil } func (a managedServiceIdentityAuth) isApplicable(b Builder) bool { // Per the Azure SDK: if the Endpoint and Sender are present this is App Service/Function Apps // which we intentionally don't support at this time isAppService := os.Getenv("MSI_ENDPOINT") != "" && os.Getenv("MSI_SECRET") != "" return b.SupportsManagedServiceIdentity && !isAppService } func (a managedServiceIdentityAuth) name() string { return "Managed Service Identity" } func (a managedServiceIdentityAuth) getAuthorizationToken(sender autorest.Sender, oauth *OAuthConfig, endpoint string) (autorest.Authorizer, error) { log.Printf("[DEBUG] getAuthorizationToken with MSI msiEndpoint %q, ClientID %q for msiEndpoint %q", a.msiEndpoint, a.clientID, endpoint) if oauth.OAuth == nil { return nil, fmt.Errorf("getting Authorization Token for MSI auth: an OAuth token wasn't configured correctly; please file a bug with more details") } var spt *adal.ServicePrincipalToken var err error if a.clientID == "" { //nolint:SA1019 spt, err = adal.NewServicePrincipalTokenFromMSI(a.msiEndpoint, endpoint) if err != nil { return nil, err } } else { //nolint:SA1019 spt, err = adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(a.msiEndpoint, endpoint, a.clientID) if err != nil { return nil, fmt.Errorf("failed to get an oauth token from MSI for user assigned identity from MSI endpoint %q with client ID %q for endpoint %q: %v", a.msiEndpoint, a.clientID, endpoint, err) } } spt.SetSender(sender) auth := autorest.NewBearerAuthorizer(spt) return auth, nil } func (a managedServiceIdentityAuth) populateConfig(c *Config) error { // nothing to populate back return nil } func (a managedServiceIdentityAuth) validate() error { var err *multierror.Error if a.msiEndpoint == "" { err = multierror.Append(err, fmt.Errorf("An MSI Endpoint must be configured")) } return err.ErrorOrNil() } go-azure-helpers-0.16.0/authentication/auth_method_msi_test.go000066400000000000000000000046111404150134500245440ustar00rootroot00000000000000package authentication import "testing" func TestManagedServiceIdentity_builder(t *testing.T) { builder := Builder{ MsiEndpoint: "https://hello-world", ClientID: "some-client-id", } method, err := managedServiceIdentityAuth{}.build(builder) if err != nil { t.Fatalf("Error building MSI Identity Auth: %+v", err) } authMethod := method.(managedServiceIdentityAuth) if builder.MsiEndpoint != authMethod.msiEndpoint { t.Fatalf("Expected MSI Endpoint to be %q but got %q", builder.MsiEndpoint, authMethod.msiEndpoint) } if builder.ClientID != authMethod.clientID { t.Fatalf("Expected MSI Client ID to be %q but got %q", builder.ClientID, authMethod.clientID) } } func TestManagedServiceIdentity_isApplicable(t *testing.T) { cases := []struct { Description string Builder Builder Valid bool }{ { Description: "Empty Configuration", Builder: Builder{}, Valid: false, }, { Description: "Feature Toggled off", Builder: Builder{ SupportsManagedServiceIdentity: false, }, Valid: false, }, { Description: "Feature Toggled on", Builder: Builder{ SupportsManagedServiceIdentity: true, }, Valid: false, }, } for _, v := range cases { applicable := servicePrincipalClientSecretAuth{}.isApplicable(v.Builder) if v.Valid != applicable { t.Fatalf("Expected %q to be %t but got %t", v.Description, v.Valid, applicable) } } } func TestManagedServiceIdentity_populateConfig(t *testing.T) { config := &Config{} err := servicePrincipalClientSecretAuth{}.populateConfig(config) if err != nil { t.Fatalf("Error populating config: %s", err) } // nothing to check since it's not doing anything } func TestManagedServiceIdentity_validate(t *testing.T) { cases := []struct { Description string Config managedServiceIdentityAuth ExpectError bool }{ { Description: "Empty Configuration", Config: managedServiceIdentityAuth{}, ExpectError: true, }, { Description: "Valid Configuration", Config: managedServiceIdentityAuth{ msiEndpoint: "https://some-location", }, ExpectError: false, }, } for _, v := range cases { err := v.Config.validate() if v.ExpectError && err == nil { t.Fatalf("Expected an error for %q: didn't get one", v.Description) } if !v.ExpectError && err != nil { t.Fatalf("Expected there to be no error for %q - but got: %v", v.Description, err) } } } go-azure-helpers-0.16.0/authentication/azure_cli_access_token.go000066400000000000000000000021031404150134500250240ustar00rootroot00000000000000package authentication import ( "fmt" "log" "strings" "github.com/Azure/go-autorest/autorest/adal" "github.com/Azure/go-autorest/autorest/azure/cli" ) type azureCliAccessToken struct { ClientID string AccessToken *adal.Token } func findValidAccessTokenForTenant(tokens []cli.Token, tenantId string) (*azureCliAccessToken, error) { for _, accessToken := range tokens { token, err := accessToken.ToADALToken() if err != nil { return nil, fmt.Errorf("[DEBUG] Error converting access token to token: %+v", err) } if !strings.Contains(accessToken.Resource, "management") { log.Printf("[DEBUG] Resource %q isn't a management domain", accessToken.Resource) continue } if !strings.HasSuffix(accessToken.Authority, tenantId) { log.Printf("[DEBUG] Resource %q isn't for the correct Tenant", accessToken.Resource) continue } validAccessToken := azureCliAccessToken{ ClientID: accessToken.ClientID, AccessToken: &token, } return &validAccessToken, nil } return nil, fmt.Errorf("No Access Token was found for the Tenant ID %q", tenantId) }go-azure-helpers-0.16.0/authentication/azure_cli_access_token_test.go000066400000000000000000000120541404150134500260710ustar00rootroot00000000000000package authentication import ( "testing" "time" "github.com/Azure/go-autorest/autorest/azure/cli" ) func TestAzureFindValidAccessTokenForTenant_Expired(t *testing.T) { expirationDate := time.Now().Add(time.Minute * -1) tenantId := "c056adac-c6a6-4ddf-ab20-0f26d47f7eea" expectedToken := cli.Token{ ExpiresOn: expirationDate.Format("2006-01-02 15:04:05.999999"), AccessToken: "7cabcf30-8dca-43f9-91e6-fd56dfb8632f", TokenType: "9b10b986-7a61-4542-8d5a-9fcd96112585", RefreshToken: "4ec3874d-ee2e-4980-ba47-b5bac11ddb94", Resource: "https://management.core.windows.net/", Authority: tenantId, } tokens := []cli.Token{expectedToken} token, err := findValidAccessTokenForTenant(tokens, tenantId) if err != nil { t.Fatalf("Expected no error to be returned but got: %+v", err) } if token == nil { t.Fatalf("Expected Token to not be nil but got: %+v", token) } } func TestAzureFindValidAccessTokenForTenant_ExpiringIn(t *testing.T) { minutesToVerify := []int{1, 30, 60} for _, minute := range minutesToVerify { expirationDate := time.Now().Add(time.Minute * time.Duration(minute)) tenantId := "c056adac-c6a6-4ddf-ab20-0f26d47f7eea" expectedToken := cli.Token{ ExpiresOn: expirationDate.Format("2006-01-02 15:04:05.999999"), AccessToken: "7cabcf30-8dca-43f9-91e6-fd56dfb8632f", TokenType: "9b10b986-7a61-4542-8d5a-9fcd96112585", RefreshToken: "4ec3874d-ee2e-4980-ba47-b5bac11ddb94", Resource: "https://management.core.windows.net/", Authority: tenantId, } tokens := []cli.Token{expectedToken} token, err := findValidAccessTokenForTenant(tokens, tenantId) if err != nil { t.Fatalf("Expected no error to be returned for minute %d but got %+v", minute, err) } if token == nil { t.Fatalf("Expected Token to have a value for minute %d but it was nil", minute) } if token.AccessToken.AccessToken != expectedToken.AccessToken { t.Fatalf("Expected the Access Token to be %q for minute %d but got %q", expectedToken.AccessToken, minute, token.AccessToken.AccessToken) } if token.ClientID != expectedToken.ClientID { t.Fatalf("Expected the Client ID to be %q for minute %d but got %q", expectedToken.ClientID, minute, token.ClientID) } } } func TestAzureFindValidAccessTokenForTenant_InvalidManagementDomain(t *testing.T) { expirationDate := time.Now().Add(1 * time.Hour) tenantId := "c056adac-c6a6-4ddf-ab20-0f26d47f7eea" expectedToken := cli.Token{ ExpiresOn: expirationDate.Format("2006-01-02 15:04:05.999999"), AccessToken: "7cabcf30-8dca-43f9-91e6-fd56dfb8632f", TokenType: "9b10b986-7a61-4542-8d5a-9fcd96112585", Resource: "https://portal.azure.com/", Authority: tenantId, } tokens := []cli.Token{expectedToken} token, err := findValidAccessTokenForTenant(tokens, tenantId) if err == nil { t.Fatalf("Expected an error but didn't get one") } if token != nil { t.Fatalf("Expected Token to be nil but got: %+v", token) } } func TestAzureFindValidAccessTokenForTenant_DifferentTenant(t *testing.T) { expirationDate := time.Now().Add(1 * time.Hour) expectedToken := cli.Token{ ExpiresOn: expirationDate.Format("2006-01-02 15:04:05.999999"), AccessToken: "7cabcf30-8dca-43f9-91e6-fd56dfb8632f", TokenType: "9b10b986-7a61-4542-8d5a-9fcd96112585", Resource: "https://management.core.windows.net/", Authority: "9b5095de-5496-4b5e-9bc6-ef2c017b9d35", } tokens := []cli.Token{expectedToken} token, err := findValidAccessTokenForTenant(tokens, "c056adac-c6a6-4ddf-ab20-0f26d47f7eea") if err == nil { t.Fatalf("Expected an error but didn't get one") } if token != nil { t.Fatalf("Expected Token to be nil but got: %+v", token) } } func TestAzureFindValidAccessTokenForTenant_Valid(t *testing.T) { expirationDate := time.Now().Add(1 * time.Hour) tenantId := "c056adac-c6a6-4ddf-ab20-0f26d47f7eea" expectedToken := cli.Token{ ExpiresOn: expirationDate.Format("2006-01-02 15:04:05.999999"), AccessToken: "7cabcf30-8dca-43f9-91e6-fd56dfb8632f", TokenType: "9b10b986-7a61-4542-8d5a-9fcd96112585", RefreshToken: "4ec3874d-ee2e-4980-ba47-b5bac11ddb94", Resource: "https://management.core.windows.net/", Authority: tenantId, } tokens := []cli.Token{expectedToken} token, err := findValidAccessTokenForTenant(tokens, tenantId) if err != nil { t.Fatalf("Expected no error to be returned but got %+v", err) } if token == nil { t.Fatalf("Expected Token to have a value but it was nil") } if token.AccessToken.AccessToken != expectedToken.AccessToken { t.Fatalf("Expected the Access Token to be %q but got %q", expectedToken.AccessToken, token.AccessToken.AccessToken) } if token.ClientID != expectedToken.ClientID { t.Fatalf("Expected the Client ID to be %q but got %q", expectedToken.ClientID, token.ClientID) } } func TestAzureFindValidAccessTokenForTenant_NoTokens(t *testing.T) { tokens := make([]cli.Token, 0) token, err := findValidAccessTokenForTenant(tokens, "abc123") if err == nil { t.Fatalf("Expected an error but didn't get one") } if token != nil { t.Fatalf("Expected a null token to be returned but got: %+v", token) } }go-azure-helpers-0.16.0/authentication/azure_cli_profile_multi_tenant.go000066400000000000000000000017501404150134500266150ustar00rootroot00000000000000package authentication import ( "strings" "github.com/Azure/go-autorest/autorest/azure/cli" ) type azureCLIProfileMultiTenant struct { profile cli.Profile clientId string environment string subscriptionId string tenantId string auxiliaryTenantIDs []string } func (a *azureCLIProfileMultiTenant) populateFields() error { // ensure we know the Subscription ID - since it's needed for everything else if a.subscriptionId == "" { err := a.populateSubscriptionID() if err != nil { return err } } // always pull the environment from the Azure CLI, since the Access Token's associated with it return a.populateEnvironment() } func (a *azureCLIProfileMultiTenant) verifyAuthenticatedAsAUser() bool { for _, subscription := range a.profile.Subscriptions { if subscription.User == nil { continue } authenticatedAsAUser := strings.EqualFold(subscription.User.Type, "user") if authenticatedAsAUser { return true } } return false } go-azure-helpers-0.16.0/authentication/azure_cli_profile_multi_tenant_population.go000066400000000000000000000042631404150134500310710ustar00rootroot00000000000000package authentication import ( "fmt" "strings" "github.com/Azure/go-autorest/autorest/azure/cli" ) func (a *azureCLIProfileMultiTenant) populateSubscriptionID() error { subscriptionId, err := a.findDefaultSubscriptionId() if err != nil { return err } a.subscriptionId = subscriptionId return nil } func (a *azureCLIProfileMultiTenant) populateTenantID() error { subscription, err := a.findSubscription(a.subscriptionId) if err != nil { return err } a.tenantId = subscription.TenantID return nil } func (a *azureCLIProfileMultiTenant) populateClientId() error { // we can now pull out the ClientID and the Access Token to use from the Access Token tokensPath, err := cli.AccessTokensPath() if err != nil { return fmt.Errorf("Error loading the Tokens Path from the Azure CLI: %+v", err) } tokens, err := cli.LoadTokens(tokensPath) if err != nil { return fmt.Errorf("No Authorization Tokens were found - please ensure the Azure CLI is installed and then log-in with `az login`.") } validToken, err := findValidAccessTokenForTenant(tokens, a.tenantId) if err != nil { return fmt.Errorf("No Authorization Tokens were found - please re-authenticate using `az login`.") } token := *validToken a.clientId = token.ClientID return nil } func (a *azureCLIProfileMultiTenant) populateEnvironment() error { subscription, err := a.findSubscription(a.subscriptionId) if err != nil { return err } a.environment = normalizeEnvironmentName(subscription.EnvironmentName) return nil } func (a azureCLIProfileMultiTenant) findDefaultSubscriptionId() (string, error) { for _, subscription := range a.profile.Subscriptions { if subscription.IsDefault { return subscription.ID, nil } } return "", fmt.Errorf("No Subscription was Marked as Default in the Azure Profile.") } func (a azureCLIProfileMultiTenant) findSubscription(subscriptionId string) (*cli.Subscription, error) { for _, subscription := range a.profile.Subscriptions { if strings.EqualFold(subscription.ID, subscriptionId) { return &subscription, nil } } return nil, fmt.Errorf("Subscription %q was not found in your Azure CLI credentials. Please verify it exists in `az account list`.", subscriptionId) } go-azure-helpers-0.16.0/authentication/azure_cli_profile_multi_tenant_population_test.go000066400000000000000000000054621404150134500321320ustar00rootroot00000000000000package authentication import ( "testing" "github.com/Azure/go-autorest/autorest/azure/cli" ) func TestAzureCliProfileMultiTenant_populateSubscriptionIdMissing(t *testing.T) { cliProfile := azureCLIProfileMultiTenant{ profile: cli.Profile{ Subscriptions: []cli.Subscription{}, }, } err := cliProfile.populateSubscriptionID() if err == nil { t.Fatalf("Expected an error to be returned - but didn't get one") } } func TestAzureCliProfileMultiTenant_populateSubscriptionIdNoDefault(t *testing.T) { cliProfile := azureCLIProfileMultiTenant{ profile: cli.Profile{ Subscriptions: []cli.Subscription{ { IsDefault: false, ID: "abc123", }, }, }, } err := cliProfile.populateSubscriptionID() if err == nil { t.Fatalf("Expected an error to be returned - but didn't get one") } } func TestAzureCliProfileMultiTenant_populateSubscriptionIdValid(t *testing.T) { subscriptionId := "abc123" cliProfile := azureCLIProfileMultiTenant{ profile: cli.Profile{ Subscriptions: []cli.Subscription{ { IsDefault: true, ID: subscriptionId, }, }, }, } err := cliProfile.populateSubscriptionID() if err != nil { t.Fatalf("Expected no error to be returned - but got: %+v", err) } if cliProfile.subscriptionId != subscriptionId { t.Fatalf("Expected the Subscription ID to be %q but got %q", subscriptionId, cliProfile.subscriptionId) } } func TestAzureCliProfileMultiTenant_populateTenantIdEmpty(t *testing.T) { cliProfile := azureCLIProfileMultiTenant{ profile: cli.Profile{ Subscriptions: []cli.Subscription{}, }, } err := cliProfile.populateEnvironment() if err == nil { t.Fatalf("Expected an error to be returned - but didn't get one") } } func TestAzureCliProfileMultiTenant_populateTenantIdMissingSubscription(t *testing.T) { cliProfile := azureCLIProfileMultiTenant{ subscriptionId: "bcd234", profile: cli.Profile{ Subscriptions: []cli.Subscription{ { IsDefault: false, ID: "abc123", }, }, }, } err := cliProfile.populateTenantID() if err == nil { t.Fatalf("Expected an error to be returned - but didn't get one") } } func TestAzureCliProfileMultiTenant_populateTenantIdValid(t *testing.T) { cliProfile := azureCLIProfileMultiTenant{ subscriptionId: "abc123", profile: cli.Profile{ Subscriptions: []cli.Subscription{ { IsDefault: false, ID: "abc123", TenantID: "bcd234", }, }, }, } err := cliProfile.populateTenantID() if err != nil { t.Fatalf("Expected no error to be returned - but got: %+v", err) } if cliProfile.subscriptionId != "abc123" { t.Fatalf("Expected Subscription ID to be 'abc123' - got %q", cliProfile.subscriptionId) } if cliProfile.tenantId != "bcd234" { t.Fatalf("Expected Tenant ID to be 'bcd234' - got %q", cliProfile.tenantId) } } go-azure-helpers-0.16.0/authentication/azure_sp_objectid.go000066400000000000000000000025531404150134500240320ustar00rootroot00000000000000package authentication import ( "context" "fmt" "github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac" "github.com/hashicorp/go-azure-helpers/sender" ) func buildServicePrincipalObjectIDFunc(c *Config) func(ctx context.Context) (string, error) { return func(ctx context.Context) (string, error) { env, err := AzureEnvironmentByNameFromEndpoint(ctx, c.MetadataHost, c.Environment) if err != nil { return "", err } s := sender.BuildSender("GoAzureHelpers") oauthConfig, err := c.BuildOAuthConfig(env.ActiveDirectoryEndpoint) if err != nil { return "", err } // Graph Endpoints graphEndpoint := env.GraphEndpoint graphAuth, err := c.GetAuthorizationToken(s, oauthConfig, env.GraphEndpoint) if err != nil { return "", err } client := graphrbac.NewServicePrincipalsClientWithBaseURI(graphEndpoint, c.TenantID) client.Authorizer = graphAuth client.Sender = s filter := fmt.Sprintf("appId eq '%s'", c.ClientID) listResult, listErr := client.List(ctx, filter) if listErr != nil { return "", fmt.Errorf("Error listing Service Principals: %#v", listErr) } if listResult.Values() == nil || len(listResult.Values()) != 1 || listResult.Values()[0].ObjectID == nil { return "", fmt.Errorf("Unexpected Service Principal query result: %#v", listResult.Values()) } return *listResult.Values()[0].ObjectID, nil } } go-azure-helpers-0.16.0/authentication/builder.go000066400000000000000000000063511404150134500217650ustar00rootroot00000000000000package authentication import ( "context" "fmt" "log" ) var ( authenticatedObjectCache = "" ) // Builder supports all of the possible Authentication values and feature toggles // required to build a working Config for Authentication purposes. type Builder struct { // Core ClientID string SubscriptionID string TenantID string TenantOnly bool Environment string MetadataHost string // Auxiliary tenant IDs used for multi tenant auth SupportsAuxiliaryTenants bool AuxiliaryTenantIDs []string // The custom Resource Manager Endpoint which should be used // only applicable for Azure Stack at this time. CustomResourceManagerEndpoint string // Azure CLI Tokens Auth SupportsAzureCliToken bool // Managed Service Identity Auth SupportsManagedServiceIdentity bool MsiEndpoint string // Service Principal (Client Cert) Auth SupportsClientCertAuth bool ClientCertPath string ClientCertPassword string // Service Principal (Client Secret) Auth SupportsClientSecretAuth bool ClientSecret string ClientSecretDocsLink string } // Build takes the configuration from the Builder and builds up a validated Config // for authenticating with Azure func (b Builder) Build() (*Config, error) { config := Config{ ClientID: b.ClientID, SubscriptionID: b.SubscriptionID, TenantID: b.TenantID, AuxiliaryTenantIDs: b.AuxiliaryTenantIDs, Environment: b.Environment, MetadataHost: b.MetadataHost, CustomResourceManagerEndpoint: b.CustomResourceManagerEndpoint, } // NOTE: the ordering here is important // since the Azure CLI Parsing should always be the last thing checked supportedAuthenticationMethods := []authMethod{ servicePrincipalClientCertificateAuth{}, servicePrincipalClientSecretMultiTenantAuth{}, servicePrincipalClientSecretAuth{}, managedServiceIdentityAuth{}, azureCliTokenMultiTenantAuth{}, azureCliTokenAuth{}, } for _, method := range supportedAuthenticationMethods { name := method.name() log.Printf("Testing if %s is applicable for Authentication..", name) // does not support it via validate? if !method.isApplicable(b) { continue } log.Printf("Using %s for Authentication", name) auth, err := method.build(b) if err != nil { return nil, err } // populate authentication specific fields on the Config // (e.g. is service principal, fields parsed from the azure cli) err = auth.populateConfig(&config) if err != nil { return nil, err } config.authMethod = auth // Authenticated Object ID Cache if config.GetAuthenticatedObjectID != nil { uncachedFunction := config.GetAuthenticatedObjectID config.GetAuthenticatedObjectID = func(ctx context.Context) (string, error) { if authenticatedObjectCache == "" { authenticatedObjectCache, err = uncachedFunction(ctx) if err != nil { return "", err } log.Printf("authenticated object ID cache miss, populating with: %q", authenticatedObjectCache) } return authenticatedObjectCache, nil } } return &config, config.authMethod.validate() } return nil, fmt.Errorf("No supported authentication methods were found!") } go-azure-helpers-0.16.0/authentication/config.go000066400000000000000000000103501404150134500215760ustar00rootroot00000000000000package authentication import ( "context" "fmt" "log" "strings" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/adal" ) // Config is the configuration structure used to instantiate a // new Azure management client. type Config struct { ClientID string SubscriptionID string TenantID string AuxiliaryTenantIDs []string Environment string MetadataHost string GetAuthenticatedObjectID func(context.Context) (string, error) AuthenticatedAsAServicePrincipal bool // A Custom Resource Manager Endpoint // at this time this should only be applicable for Azure Stack. CustomResourceManagerEndpoint string authMethod authMethod } type OAuthConfig struct { OAuth *adal.OAuthConfig MultiTenantOauth *adal.MultiTenantOAuthConfig } // GetAuthorizationToken returns an authorization token for the authentication method defined in the Config func (c Config) GetOAuthConfig(activeDirectoryEndpoint string) (*adal.OAuthConfig, error) { log.Printf("Getting OAuth config for endpoint %s with tenant %s", activeDirectoryEndpoint, c.TenantID) // fix for ADFS environments, if the login endpoint ends in `/adfs` it's an adfs environment // the login endpoint ends up residing in `ActiveDirectoryEndpoint` oAuthTenant := c.TenantID if strings.HasSuffix(strings.ToLower(activeDirectoryEndpoint), "/adfs") { log.Printf("[DEBUG] ADFS environment detected - overriding Tenant ID to `adfs`!") oAuthTenant = "adfs" } oauth, err := adal.NewOAuthConfig(activeDirectoryEndpoint, oAuthTenant) if err != nil { return nil, err } // OAuthConfigForTenant returns a pointer, which can be nil. if oauth == nil { return nil, fmt.Errorf("Unable to configure OAuthConfig for tenant %s", c.TenantID) } return oauth, nil } // GetMultiTenantOAuthConfig returns a multi-tenant authorization token for the authentication method defined in the Config func (c Config) GetMultiTenantOAuthConfig(activeDirectoryEndpoint string) (*adal.MultiTenantOAuthConfig, error) { log.Printf("Getting multi OAuth config for endpoint %s with tenant %s (aux tenants: %v)", activeDirectoryEndpoint, c.TenantID, c.AuxiliaryTenantIDs) oauth, err := adal.NewMultiTenantOAuthConfig(activeDirectoryEndpoint, c.TenantID, c.AuxiliaryTenantIDs, adal.OAuthOptions{}) if err != nil { return nil, err } // OAuthConfigForTenant returns a pointer, which can be nil. if oauth == nil { return nil, fmt.Errorf("Unable to configure OAuthConfig for tenant %s (auxiliary tenants %v)", c.TenantID, c.AuxiliaryTenantIDs) } return &oauth, nil } // BuildOAuthConfig builds the authorization configuration for the specified Active Directory Endpoint func (c Config) BuildOAuthConfig(activeDirectoryEndpoint string) (*OAuthConfig, error) { multiAuth := OAuthConfig{} var err error multiAuth.OAuth, err = c.GetOAuthConfig(activeDirectoryEndpoint) if err != nil { return nil, err } if len(c.AuxiliaryTenantIDs) > 0 { multiAuth.MultiTenantOauth, err = c.GetMultiTenantOAuthConfig(activeDirectoryEndpoint) if err != nil { return nil, err } } return &multiAuth, nil } // BearerAuthorizerCallback returns a BearerAuthorizer valid only for the Primary Tenant // this signs a request using the AccessToken returned from the primary Resource Manager authorizer func (c Config) BearerAuthorizerCallback(sender autorest.Sender, oauthConfig *OAuthConfig) *autorest.BearerAuthorizerCallback { return autorest.NewBearerAuthorizerCallback(sender, func(tenantID, resource string) (*autorest.BearerAuthorizer, error) { // a BearerAuthorizer is only valid for the primary tenant newAuthConfig := &OAuthConfig{ OAuth: oauthConfig.OAuth, } storageSpt, err := c.GetAuthorizationToken(sender, newAuthConfig, resource) if err != nil { return nil, err } cast, ok := storageSpt.(*autorest.BearerAuthorizer) if !ok { return nil, fmt.Errorf("Error converting %+v to a BearerAuthorizer", storageSpt) } return cast, nil }) } // GetAuthorizationToken returns an authorization token for the authentication method defined in the Config func (c Config) GetAuthorizationToken(sender autorest.Sender, oauth *OAuthConfig, endpoint string) (autorest.Authorizer, error) { return c.authMethod.getAuthorizationToken(sender, oauth, endpoint) } go-azure-helpers-0.16.0/authentication/environment.go000066400000000000000000000166731404150134500227130ustar00rootroot00000000000000package authentication import ( "context" "encoding/json" "fmt" "net/http" "strings" "github.com/Azure/go-autorest/autorest/azure" ) var sdkEnvironmentLookupMap = map[string]azure.Environment{ "public": azure.PublicCloud, "usgovernment": azure.USGovernmentCloud, "german": azure.GermanCloud, "china": azure.ChinaCloud, } type Environment struct { Portal string `json:"portal"` Authentication Authentication `json:"authentication"` Media string `json:"media"` GraphAudience string `json:"graphAudience"` Graph string `json:"graph"` Name string `json:"name"` Suffixes Suffixes `json:"suffixes"` Batch string `json:"batch"` ResourceManager string `json:"resourceManager"` VmImageAliasDoc string `json:"vmImageAliasDoc"` ActiveDirectoryDataLake string `json:"activeDirectoryDataLake"` SqlManagement string `json:"sqlManagement"` Gallery string `json:"gallery"` } type Authentication struct { LoginEndpoint string `json:"loginEndpoint"` Audiences []string `json:"audiences"` Tenant string `json:"tenant"` IdentityProvider string `json:"identityProvider"` } type Suffixes struct { AzureDataLakeStoreFileSystem string `json:"azureDataLakeStoreFileSystem"` AcrLoginServer string `json:"acrLoginServer"` SqlServerHostname string `json:"sqlServerHostname"` AzureDataLakeAnalyticsCatalogAndJob string `json:"azureDataLakeAnalyticsCatalogAndJob"` KeyVaultDns string `json:"keyVaultDns"` Storage string `json:"storage"` AzureFrontDoorEndpointSuffix string `json:"azureFrontDoorEndpointSuffix"` } // DetermineEnvironment determines what the Environment name is within // the Azure SDK for Go and then returns the association environment, if it exists. func DetermineEnvironment(name string) (*azure.Environment, error) { // detect cloud from environment env, envErr := azure.EnvironmentFromName(name) if envErr != nil { // try again with wrapped value to support readable values like german instead of AZUREGERMANCLOUD wrapped := fmt.Sprintf("AZURE%sCLOUD", name) env, envErr = azure.EnvironmentFromName(wrapped) if envErr != nil { return nil, fmt.Errorf("An Azure Environment with name %q was not found: %+v", name, envErr) } } return &env, nil } // LoadEnvironmentFromUrl attempts to load the specified environment from the endpoint. // if the endpoint is an empty string, or an environment can't be // found at the endpoint url then an error is returned func LoadEnvironmentFromUrl(endpoint string) (*azure.Environment, error) { if endpoint == "" { return nil, fmt.Errorf("Endpoint was not set!") } env, err := azure.EnvironmentFromURL(endpoint) if err != nil { return nil, fmt.Errorf("Error retrieving Environment from Endpoint %q: %+v", endpoint, err) } return &env, nil } func normalizeEnvironmentName(input string) string { // Environment is stored as `Azure{Environment}Cloud` output := strings.ToLower(input) output = strings.TrimPrefix(output, "azure") output = strings.TrimSuffix(output, "cloud") // however Azure Public is `AzureCloud` in the CLI Profile and not `AzurePublicCloud`. if output == "" { return "public" } return output } // AzureEnvironmentByName returns a specific Azure Environment from the specified endpoint func AzureEnvironmentByNameFromEndpoint(ctx context.Context, endpoint string, environmentName string) (*azure.Environment, error) { if env, ok := sdkEnvironmentLookupMap[strings.ToLower(environmentName)]; ok { return &env, nil } if endpoint == "" { return nil, fmt.Errorf("unable to locate metadata for environment %q from the built in `public`, `usgoverment`, `china` and no custom metadata host has been specified", environmentName) } environments, err := getSupportedEnvironments(ctx, endpoint) if err != nil { return nil, err } // while the array contains values for _, env := range environments { if strings.EqualFold(env.Name, environmentName) { aEnv, err := buildAzureEnvironment(env) if err != nil { return nil, err } return aEnv, nil } } return nil, fmt.Errorf("unable to locate metadata for environment %q from custom metadata host %q", environmentName, endpoint) } // IsEnvironmentAzureStack returns whether a specific Azure Environment is an Azure Stack environment func IsEnvironmentAzureStack(ctx context.Context, endpoint string, environmentName string) (bool, error) { if _, ok := sdkEnvironmentLookupMap[strings.ToLower(environmentName)]; ok { return false, nil } environments, err := getSupportedEnvironments(ctx, endpoint) if err != nil { return false, err } // while the array contains values for _, env := range environments { if err != nil { return false, fmt.Errorf("unable to decode environment from %q response: %+v", endpoint, err) } if strings.EqualFold(env.Name, environmentName) { if !strings.EqualFold(env.Authentication.IdentityProvider, "AAD") || !strings.EqualFold(env.Authentication.Tenant, "common") { return true, nil } return false, nil } } return false, fmt.Errorf("unable to find environment %q from endpoint %q", environmentName, endpoint) } func getSupportedEnvironments(ctx context.Context, endpoint string) ([]Environment, error) { uri := fmt.Sprintf("https://%s/metadata/endpoints?api-version=2020-06-01", endpoint) client := http.Client{ Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, }, } req, err := http.NewRequestWithContext(ctx, "GET", uri, nil) if err != nil { return nil, err } resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("retrieving environments from Azure MetaData service: %+v", err) } var environments []Environment if err := json.NewDecoder(resp.Body).Decode(&environments); err != nil { return nil, err } return environments, nil } func buildAzureEnvironment(env Environment) (*azure.Environment, error) { aEnv := &azure.Environment{ Name: env.Name, ResourceManagerEndpoint: env.ResourceManager, StorageEndpointSuffix: env.Suffixes.Storage, ActiveDirectoryEndpoint: env.Authentication.LoginEndpoint, GraphEndpoint: env.Graph, KeyVaultEndpoint: fmt.Sprintf("https://%s/", env.Suffixes.KeyVaultDns), GalleryEndpoint: env.Gallery, BatchManagementEndpoint: env.Batch, SQLDatabaseDNSSuffix: env.Suffixes.SqlServerHostname, KeyVaultDNSSuffix: env.Suffixes.KeyVaultDns, ContainerRegistryDNSSuffix: env.Suffixes.AcrLoginServer, ResourceIdentifiers: azure.ResourceIdentifier{ // This isn't returned from the metadata url and is universal across all environments Storage: "https://storage.azure.com/", Graph: env.Graph, KeyVault: fmt.Sprintf("https://%s/", env.Suffixes.KeyVaultDns), Datalake: env.ActiveDirectoryDataLake, Batch: env.Batch, Synapse: azure.NotAvailable, ServiceBus: azure.NotAvailable, OperationalInsights: azure.NotAvailable, }, } if len(env.Authentication.Audiences) > 0 { aEnv.TokenAudience = env.Authentication.Audiences[0] } else { return nil, fmt.Errorf("unable to find token audience for environment %q", env.Name) } return aEnv, nil } go-azure-helpers-0.16.0/authentication/environment_test.go000066400000000000000000000076201404150134500237420ustar00rootroot00000000000000package authentication import ( "context" "strings" "testing" ) func TestAzureEnvironmentNames(t *testing.T) { testData := map[string]string{ "": "public", "AzureChinaCloud": "china", "AzureCloud": "public", "AzureGermanCloud": "german", "AZUREUSGOVERNMENTCLOUD": "usgovernment", "AzurePublicCloud": "public", } for input, expected := range testData { actual := normalizeEnvironmentName(input) if actual != expected { t.Fatalf("Expected %q for input %q: got %q!", expected, input, actual) } } } func TestAccAzureEnvironmentByName(t *testing.T) { env, err := AzureEnvironmentByNameFromEndpoint(context.TODO(), "management.azure.com", "public") if err != nil { t.Fatalf("Error getting Endpoint: %s", err) } if !strings.EqualFold(env.Name, "AzurePublicCloud") { t.Fatalf("Incorrect environment name returned. Expected: %q. Received: %q", "AzurePublicCloud", env.Name) } env, err = AzureEnvironmentByNameFromEndpoint(context.TODO(), "management.azure.com", "usgovernment") if err != nil { t.Fatalf("Error getting Endpoint: %s", err) } if !strings.EqualFold(env.Name, "AzureUSGovernmentCloud") { t.Fatalf("Incorrect environment name returned. Expected: %q. Received: %q", "AzureUSGovernmentCloud", env.Name) } env, err = AzureEnvironmentByNameFromEndpoint(context.TODO(), "management.azure.com", "german") if err != nil { t.Fatalf("Error getting Endpoint: %s", err) } if !strings.EqualFold(env.Name, "AzureGermanCloud") { t.Fatalf("Incorrect environment name returned. Expected: %q. Received: %q", "AzureGermanCloud", env.Name) } env, err = AzureEnvironmentByNameFromEndpoint(context.TODO(), "management.azure.com", "china") if err != nil { t.Fatalf("Error getting Endpoint: %s", err) } if !strings.EqualFold(env.Name, "AzureChinaCloud") { t.Fatalf("Incorrect environment name returned. Expected: %q. Received: %q", "AzureChinaCloud", env.Name) } } func TestAccAzureEnvironmentByNameFromEndpoint(t *testing.T) { env, err := AzureEnvironmentByNameFromEndpoint(context.TODO(), "management.azure.com", "AzureCloud") if err != nil { t.Fatalf("Error getting Endpoint: %s", err) } if !strings.EqualFold(env.Name, "AzureCloud") { t.Fatalf("Incorrect environment name returned. Expected: %q. Received: %q", "AzureCloud", env.Name) } env, err = AzureEnvironmentByNameFromEndpoint(context.TODO(), "management.azure.com", "AzureChinaCloud") if err != nil { t.Fatalf("Error getting Endpoint: %s", err) } if !strings.EqualFold(env.Name, "AzureChinaCloud") { t.Fatalf("Incorrect environment name returned. Expected: %q. Received: %q", "AzureChinaCloud", env.Name) } env, err = AzureEnvironmentByNameFromEndpoint(context.TODO(), "management.azure.com", "AzureUSGovernment") if err != nil { t.Fatalf("Error getting Endpoint: %s", err) } if !strings.EqualFold(env.Name, "AzureUSGovernment") { t.Fatalf("Incorrect environment name returned. Expected: %q. Received: %q", "AzureUSGovernment", env.Name) } env, err = AzureEnvironmentByNameFromEndpoint(context.TODO(), "management.azure.com", "AzureGermanCloud") if err != nil { t.Fatalf("Error getting Endpoint: %s", err) } if !strings.EqualFold(env.Name, "AzureGermanCloud") { t.Fatalf("Incorrect environment name returned. Expected: %q. Received: %q", "AzureGermanCloud", env.Name) } _, err = AzureEnvironmentByNameFromEndpoint(context.TODO(), "badurl", "AzureGermanCloud") if err == nil { t.Fatal("Expected error from bad endpoint") } _, err = AzureEnvironmentByNameFromEndpoint(context.TODO(), "management.azure.com", "badEnvironment") if err == nil { t.Fatal("Expected error from bad environment") } } func TestAccIsEnvironmentAzureStack(t *testing.T) { ok, err := IsEnvironmentAzureStack(context.TODO(), "management.azure.com", "public") if err != nil { t.Fatalf("Error getting Endpoint: %s", err) } if ok { t.Fatal("Expected `public` environment to not be Azure Stack") } } go-azure-helpers-0.16.0/authentication/testdata/000077500000000000000000000000001404150134500216145ustar00rootroot00000000000000go-azure-helpers-0.16.0/authentication/testdata/empty000066400000000000000000000000001404150134500226630ustar00rootroot00000000000000go-azure-helpers-0.16.0/authentication/testdata/sp.crt000066400000000000000000000034321404150134500227520ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFETCCAvkCFDIVXQcARcTOdtv85iBttDDWyrS9MA0GCSqGSIb3DQEBCwUAMEUx CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl cm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMjAwODI3MDUyMDA3WhcNMjEwODI3MDUy MDA3WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOC Ag8AMIICCgKCAgEA09MsHIc11fjLn/ahhl8Ghfr9mQVrCDfUwUHCcEIzJil+PbSl dHdacJO93BJSGDpekBS+Gvudaj01KEat1OPPtR2CpTIFLuKB5WGIsCf9xlX25MOS aucYd5ExrkT3ivrZLdgVJ+/0aIs5GTBtFBxNtR6+fzCmXY26lq5e3tyzlI7GyOaj CcbuL7DW24li4uW9649Bjq+9bo6K0gYtY8EIS+mr/IhrvTjL2VFxOA28hUo1D2Pr dqZOSHHz/IMK14g6R2Xn7w6wwr5gyOYCPuoHOtj1lQ913zfU7HpBaaZ6pVA1ZPut TyGxSp1giBrkoDT0N/axxXBWUuLfLKoxHENd+th971x1aYChnkqMdx8v6Es37+Gy S/IdEHSpRviH/iF1U1LXNQ0LDb5aDFe6PARw7VLSMwCa3aOmXrc7SYMmaakFX5Dc sR+/+lHE5lM4DShFySRX4ncL5ZlD5A69NZRjfyhWk9djuLeTFqlLPU1kpYvD93c6 fIcs84K6br2jrDZQNanz/mQlepZqexCmOiBZrf1zHXGRYi+u4lUmrZflAPW4rTPT 62yCorDqkTsLyefhvIQXiBmkgjQ8UWnjoC39SGpmZ31LH+vm1IpEpsT2/NFa1jcC MfbyV77qdWWQfc0LKB+OstLKHv6S4+k9e0KvhvvMo/psh/f5tEQpqHX8bzkCAwEA ATANBgkqhkiG9w0BAQsFAAOCAgEATVp3xsUpif9h9K3oQ+ErccWxnhOu1q4o66tz gCw5O14AjqlcFQMG62YtmCY8oM077dfBCq2tb+ib3jpdqvV1MUvAaPU6sRmNo8x8 S5qibDe+Fh8GKUEO4+PCi20m+6t7nhqdKHraruEJpdvPcB2yXXnZC1nMUSIzIjC7 9hw05j3Gk4s2LjDfc7eZjHqYG3ToBjv5klTXTQetFFU1DM9tUTFq823WzRG8k8A2 tRuiVeWocCVAWnTLKAaYWPEnWvaG3OO89czq5MGTqqSq0CO1KB84EN082FqdUGgA YvGxuaLOrY1ylNVhhBWEfIvN5UDgtxH1VYFEZbYGLVGrWiZECliblYWEP9ln7tKp NZQy2Zxwn4iOo58n8+UAnmxWxVKGky3V6T+vmKikIzwuVYaH7zbpD4Cax8uyl2zo jaXIrrfA0H7YYzNcALFducBTE23/L9G2nKbtF6AwQ0PeuKaDl2H46/mjqV3ODWJo tOIuqs5GIBjJ+j4uXX32WvMs/bJYAq8U+RC0DaJQfOKgJAhEgzBrA+W9C5NyWW6w BQ7o8U+zUKtnfw0ybLUcilYHwAaCNkoO8qX0nafn1kdU7RPy2InbBys0cVUpZ67G hBdbvGTlIH3fxT3MjY8FvZQuLQ7fcGHPNrjz3lR8IJB2oH/5IFGmkGfna4Gi5xc6 llWUpTI= -----END CERTIFICATE----- go-azure-helpers-0.16.0/authentication/testdata/sp.csr000066400000000000000000000032171404150134500227520ustar00rootroot00000000000000-----BEGIN CERTIFICATE REQUEST----- MIIEnzCCAocCAQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcN AQEBBQADggIPADCCAgoCggIBANPTLByHNdX4y5/2oYZfBoX6/ZkFawg31MFBwnBC MyYpfj20pXR3WnCTvdwSUhg6XpAUvhr7nWo9NShGrdTjz7UdgqUyBS7igeVhiLAn /cZV9uTDkmrnGHeRMa5E94r62S3YFSfv9GiLORkwbRQcTbUevn8wpl2NupauXt7c s5SOxsjmownG7i+w1tuJYuLlveuPQY6vvW6OitIGLWPBCEvpq/yIa704y9lRcTgN vIVKNQ9j63amTkhx8/yDCteIOkdl5+8OsMK+YMjmAj7qBzrY9ZUPdd831Ox6QWmm eqVQNWT7rU8hsUqdYIga5KA09Df2scVwVlLi3yyqMRxDXfrYfe9cdWmAoZ5KjHcf L+hLN+/hskvyHRB0qUb4h/4hdVNS1zUNCw2+WgxXujwEcO1S0jMAmt2jpl63O0mD JmmpBV+Q3LEfv/pRxOZTOA0oRckkV+J3C+WZQ+QOvTWUY38oVpPXY7i3kxapSz1N ZKWLw/d3OnyHLPOCum69o6w2UDWp8/5kJXqWansQpjogWa39cx1xkWIvruJVJq2X 5QD1uK0z0+tsgqKw6pE7C8nn4byEF4gZpII0PFFp46At/UhqZmd9Sx/r5tSKRKbE 9vzRWtY3AjH28le+6nVlkH3NCygfjrLSyh7+kuPpPXtCr4b7zKP6bIf3+bREKah1 /G85AgMBAAGgFTATBgkqhkiG9w0BCQcxBgwEMTIzNDANBgkqhkiG9w0BAQsFAAOC AgEAWdCppUzC/fesV9v28BRg+mNO1CW29wU4iNJk/SYXL0DJajJ8pRVDSI0U7eKF jL16xH4NuWmP8LwLNacClnJ2xrRTi75i5u/aEXdugcxwUUy3MTatMcUY3reuwpTu SkTbYdieZqFkuuBjUcmnCA67SfLs1jmTRoOsxTpdb/IlulojtV5rALE/GqD7B7mV pKqJO04+gBpfNixnq145rMV8A+S6X5owoTPiFDWzwYXzU5oIC29YqrJRHSz+vRyb 8OFZn98SUa4sgHj4XEk1d6nL+yQnKlQCprudlkXWNSReO4mk8/HIHu/+5v6tKuTO F50jvtowAufr0/Y54ZCiTvJhHgQwi/PKyWHravUdohokRVLWEVQwZ648o9IPcRDS trndV1TpktEG3dcwFOK6DQVcJTFmQ20i8tRyRsZB7BZNQ4PIsF+A2blDtxwayZv8 VV3DcNRLGAQNH7t4m7D/+htz3+2RAnNw8ZWMEe7b4MSF09JG7sxI6y6AJ1MIQWaR uS3OwxX7VLWb4kx1UZNN6qXr4Y4gJMHZ+2u+zxWJEo4j7dmythYya6dP01pchrMV o67kdqgZtJnFVnpY9P7yJ1vSctaAcSJVvigbbrOT15utpeim7C2iK1Cq7zDKangM LsSph59iU8HewfXaOD1fg3OtS5zVnjQs7TsYPL4Wq2J16gg= -----END CERTIFICATE REQUEST----- go-azure-helpers-0.16.0/authentication/testdata/sp.key000066400000000000000000000063101404150134500227500ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDT0ywchzXV+Muf 9qGGXwaF+v2ZBWsIN9TBQcJwQjMmKX49tKV0d1pwk73cElIYOl6QFL4a+51qPTUo Rq3U48+1HYKlMgUu4oHlYYiwJ/3GVfbkw5Jq5xh3kTGuRPeK+tkt2BUn7/RoizkZ MG0UHE21Hr5/MKZdjbqWrl7e3LOUjsbI5qMJxu4vsNbbiWLi5b3rj0GOr71ujorS Bi1jwQhL6av8iGu9OMvZUXE4DbyFSjUPY+t2pk5IcfP8gwrXiDpHZefvDrDCvmDI 5gI+6gc62PWVD3XfN9TsekFppnqlUDVk+61PIbFKnWCIGuSgNPQ39rHFcFZS4t8s qjEcQ1362H3vXHVpgKGeSox3Hy/oSzfv4bJL8h0QdKlG+If+IXVTUtc1DQsNvloM V7o8BHDtUtIzAJrdo6ZetztJgyZpqQVfkNyxH7/6UcTmUzgNKEXJJFfidwvlmUPk Dr01lGN/KFaT12O4t5MWqUs9TWSli8P3dzp8hyzzgrpuvaOsNlA1qfP+ZCV6lmp7 EKY6IFmt/XMdcZFiL67iVSatl+UA9bitM9PrbIKisOqROwvJ5+G8hBeIGaSCNDxR aeOgLf1IamZnfUsf6+bUikSmxPb80VrWNwIx9vJXvup1ZZB9zQsoH46y0soe/pLj 6T17Qq+G+8yj+myH9/m0RCmodfxvOQIDAQABAoICAHnj+m4wH/qQwBu8gBYieE6A r41uYLjJ59ONU6XlcilzlwFLAiqSw6gkXAxXPoZSkDe4t+wu/dY0gnI6uazXqp49 7P/CWNkx3i1wgodbe9y1eu0I3ShG8v1av4vYg9mln3OR9BqXKb0+4AxuoVYBM3YV yujFxfXvqH8st8rmlS/XOOxCS29E2ar3x6ts00pdiXwTY1YIKqmDvL4+b8AHRA7q 19zpY5XvKN5UzyHCRcktenrwtH4CFOn7KLsVbRe5fE+5hE071ts5UdhJAuHRKXfj gO3Vh657Ijsx3pI9CjY2gYMqkAAJE0wsY2/uicDmt+G8Q2+pxQLNKj85cCkjWuAB AK4kEhW7hC6o1C0OlDf0NAhvHZvAHno+Dn1fWuSbcuCuZk6t9Pj7EDhZ9wfUwMep DODAXV0D6y/WbtSoiVeFSBW4MyvDQrl5OrXO5jdGAalsJcwRVE9DdGDkWCfnnAJi qCta7zxHy5/2zLy9EFNrSd2xf7zAIQapXO2TmfC1VK4klAKCCR/r2CCnwx1515Zu YPCM0wf3XsnD44BIiJv6G/riRUM2UAd1Tn6xc/yLkk2APfq8rOP5fH/jpc2yM4ra TTljExdc7C0mhq6RHxXogrKIl96NXoA61qMOz4DibeLJi/9LEerOhErZnQCJK9qW TEl8c0jnUnaD6ZsFPGkBAoIBAQD72XLbwrVRS5dyZcsqk/zumg+/+Iw6nmRbLh5X 1Uy+bxAIcTdxgnGpKTwQdBPImRDtep5svYtjVtgRRefUUN+f7d11VLjH/ZBPEMiG fycvhbUnl/fxKOlZwy2EQambpxqiqfzCB8mDNGUJW+D8F72UH58Pu7x2QBghzCNm gZBhiD413Yz4NIL10dHacIcHEIH8E29XQADH7Ogobqg8hZtNUY2QNijb/BGCEdIy ljDmyZJVzm5/PNVsnqZKRRA/C+I4yMP+EcSSfAokwF27qRJpwIuF4IlaG+oa5szV r6cNayQ7Mbz8CFoHwLtg/0VD+3J/XCF0GzmprzEsolab3N2dAoIBAQDXUNxETtKB Dda/zXX00U/9EJfiCZ0xgYjS7qUcyUHoPcpJlzZGP3dfF+sJXnllYiYvjEyx2rt8 +C0IJ0SiI9k5NbyKCdgKi83aYUBhxAskkbRQfs0mMkS/pBddPQLncJ8umpLvYaRS noFiX8Ast8cWZdUDspID6CP4T/+iPf4vU9YL6d0f1V3MUAXb7VP5iVwwWl2HA8xt QEk5/PYtlht0+Zuvx6W0IOu6ztXXkMOxKc6aYsRFF598WBe8Mz5rZ5b5u+qi33ac aDlhXQqjxHkR9SAl0cej6t0fWgUQIeZgKbNcEACm9YmooD3VoWn6ffclZOPIRHQH 8QeqJ6OlLbNNAoIBAF9iMssiijN48qnj/bdphxEDDlYBl3KYDKAv7lQLt94kCCl6 X6pA4jVUJaiBngCKYEvg9eFvRDxiZsDb/fp+isNyfj5y0O0FjsMzMw8lVzz7YenV 95TBjudtMW72w3rtJfxXbyA5fMuRa3bI8oPnpehUtZRq4OzUR499Vib7iSg3RoB2 IW7bzIG3bshReAJn8SL8ZV3hIqqVgjPV/Se23mPBMGFe8cinRiZCA58fHPQ7gY23 +kd8TcSurYJRd9647HvfJcOzDQUBr4HNzXJGjW+5+d3BdaPIELkRkqCXvygo3PcC VnYJ37cx+oW5CI2/zNSJmy3zJbqrXG9wKGctlk0CggEBAJvWOWXzCqY/JuOxSCNF 7zfA77y4qp3vG+FWuggondwCBMvYdn7HZpVb0U/5obFAJVUanVPIdRRQ5v7UtPcJ LSob+3aJA2U2pGnfI2LfKhVwo2nzLHXtwhZWJFbmVpi9yWhIlfpN2Em30PsrRALZ jG1ojJDdiJLtTENnWyTI65TDf9Mlk1Z77iR28panobSktyWD8ddLZ4TXF2ix7lc2 Im86TXUe8Y56mUKeeuVHMGukT5Ur0NQU8ehkNeGP0SiZVAsx9/Oj+svfH1CZr56y D9JwcLEOsQixbxNG6w0vC/cjl14VcvNkGRsDx108M2EKFHtrFvWWncg1qg98GxPt DK0CggEAXXVz44HmB4fM5BFUqvz0YpXHcP0PeRWgficSMBL2jXWqcx2dWjTp2W+Y +K14G8z6tZGIuTFd0Jswhr4rvPx3B6efb3W/Llp7/KyHIzaVgOAFt7/ocwo2jYfn RqSkJKmoixErODGfzpHYyPqXZ7n1LpB713npztyMkdk/QMIHdGgC5omWVOnuNose FyYhYwqXFXhx6hcEMB88yl7JmwoJFl7FNd6yqG9BmcTt3FMUSaKZVz6wbsJz2LiV sph2U6IoOWtjtBhNwjiEHwrcazC3jdBP3SA+l6tMXVrb1BJVt1G//u8ZcKM9rLaz 6VqDmen96qRP1oxlMKA+iJXOJqFfSA== -----END PRIVATE KEY----- go-azure-helpers-0.16.0/authentication/testdata/sp.pfx000066400000000000000000000077251404150134500227700ustar00rootroot0000000000000000 *H 00 *H 00 *H 0 *H  0lo=ep:!Iiﳎ[ZtƐr׉d0 = 褓@i=L [y64s:@x>b5~Dj #8xa̡f`4yˊ $ j!>ЏXHSgπ?<ͥFyfNG;Tp|v;EųM&a}:+/9.bO/V~[oeat[ɧ(q˾F[4)^{p]Wf[ I"yH"4ҷf$DۑO.^ A%bV]]grרU:c)ZgeP؝4?~Х:ӌreLFWa:1,ܹ\0jlAb'#lZf -Օ6v"q+ P:[gK!צa, AӉAyD _(:XW{>#dEͯt;H~Ӎ5 ,)F^7UnJи@^8IΠAXocF{ǏK]t!_0AZ8 W̨5I| %#tUG^tPjt iz 5V,)xOm]w+.Y{2 d"ԃOȄXLq Cy T[%*^٢w̦wI{adUteQ-eYEYR#M8ٿzxX͹\Pmw cdnrIcҚX yoFd!㞾!W3ʴgо˽A4Q28"ti[qF^~? zC/2={Q\Gj>u oPR)^7_tUf;ʅyɡa/A@Bm`@뇄I5оZ!?]ΐ3b㴠٫R̀20  *H   0 0  *H   n0 j0 *H  0 HC~pƳݷ慺N" ,gZ>> ;7xy +7. x.e{~b\Cu2++ : ~Vy߼"G, H[E/@Fʛ=.la6v5nT$9ƾZ}I?V_LcqS~U?OY8ΧKT^r ^'~_j8OF=>s:Eޭ-7Ps9]_dḛgd`p܍W4 j\]칒_*Cw#2e60$V1\ F~2&Ω>h?8NL=PO)8rЭ{:@S/lngkfƆ^'p@Y'7C©1,uA97L*Xj ez_W5D&ۑع ,Ԭ?8Q^Q[8_Q۳![!PE@l+7n㿅_f${H [i6+*nlmYM &zph+@f 'IS&U.pӚiG8?'x{2榧$4FGł y5-R8glTpU&ri^VwICf+[\zwEK}N M|Øw)ەkȷ&(*!d]of U!ErߩC –V ݚF=>n}2*9w.9£&M#kwP~[QXNÐT|=O>.:j6Џe%s<>:@foNndU-'!@՝ȸN;5Ę aBWDA4Cཷ/+L3誳ً˷<F;8T5/>.v}0 C9㺧(rQWxSC$7 8=`)iKq);h* cIܹdsb')$,R l$^UCլ>1cuӸD Vxd}_n^p).-xPnxĵA{DEܯIvS T -%ƣ.!I0JzJM<^,v|7ܿG AnK~:_mgs>P,h/}BnݖjNt *jۈlu=ZLLKDbm UlʳV=$T:<7@_⾎?:|CN t/6q^BOFR! .7bL7V--:WhE'z/ġIG `?D՝ZʚVxc(\&m&a-Ao}3mq: -6ػ$~~&KH1tف9G=*OL88{q3gO.*M^LyR{Td wP<˶ R+RZ\. g*1%0# *H  1]UmBh`ݮp010!0 +ғYng#2gn #go-azure-helpers-0.16.0/authentication/testdata/sp.pfx.alias000077700000000000000000000000001404150134500252012sp.pfxustar00rootroot00000000000000go-azure-helpers-0.16.0/go.mod000066400000000000000000000012041404150134500160670ustar00rootroot00000000000000module github.com/hashicorp/go-azure-helpers require ( github.com/Azure/azure-sdk-for-go v51.2.0+incompatible github.com/Azure/go-autorest/autorest v0.11.18 github.com/Azure/go-autorest/autorest/adal v0.9.13 github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/hashicorp/go-multierror v1.0.0 github.com/hashicorp/go-version v1.2.1 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 ) go 1.16 go-azure-helpers-0.16.0/go.sum000066400000000000000000000122411404150134500161170ustar00rootroot00000000000000github.com/Azure/azure-sdk-for-go v51.2.0+incompatible h1:qQNk//OOHK0GZcgMMgdJ4tZuuh0zcOeUkpTxjvKFpSQ= github.com/Azure/azure-sdk-for-go v51.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= 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.11.18 h1:90Y4srNYrwOtAgVo3ndrQkTYn6kf1Eg/AjTFJ8Is2aM= github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q= github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 h1:dMOmEJfkLKW/7JsokJqkyoYSgmR08hi9KrhjZb+JALY= github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 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/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= go-azure-helpers-0.16.0/polling/000077500000000000000000000000001404150134500164305ustar00rootroot00000000000000go-azure-helpers-0.16.0/polling/poller.go000066400000000000000000000005331404150134500202550ustar00rootroot00000000000000package polling import ( "context" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/azure" ) type LongRunningPoller struct { future *azure.Future ctx context.Context client autorest.Client } func (fw *LongRunningPoller) PollUntilDone() error { return fw.future.WaitForCompletionRef(fw.ctx, fw.client) } go-azure-helpers-0.16.0/resourceproviders/000077500000000000000000000000001404150134500205515ustar00rootroot00000000000000go-azure-helpers-0.16.0/resourceproviders/registration.go000066400000000000000000000043621404150134500236170ustar00rootroot00000000000000package resourceproviders import ( "context" "fmt" "log" "strings" "sync" "github.com/Azure/azure-sdk-for-go/profiles/2017-03-09/resources/mgmt/resources" ) // DetermineResourceProvidersRequiringRegistration determines which Resource Providers require registration to be able to be used func DetermineResourceProvidersRequiringRegistration(availableResourceProviders []resources.Provider, requiredResourceProviders map[string]struct{}) map[string]struct{} { providers := make(map[string]struct{}) // filter out any providers already registered and not in the required list. for _, p := range availableResourceProviders { // Skip it if it's not in the required list. if _, ok := requiredResourceProviders[*p.Namespace]; !ok { continue } // If it's in the required list but not registered. if strings.ToLower(*p.RegistrationState) != "registered" { log.Printf("[DEBUG] Adding provider registration for namespace %s\n", *p.Namespace) providers[*p.Namespace] = requiredResourceProviders[*p.Namespace] } } return providers } // RegisterForSubscription registers the specified Resource Providers in the current Subscription func RegisterForSubscription(ctx context.Context, client resources.ProvidersClient, providersToRegister map[string]struct{}) error { var err error var failedProviders []string var wg sync.WaitGroup wg.Add(len(providersToRegister)) for providerName := range providersToRegister { go func(p string) { defer wg.Done() log.Printf("[DEBUG] Registering Resource Provider %q with namespace", p) if innerErr := registerWithSubscription(ctx, p, client); innerErr != nil { failedProviders = append(failedProviders, p) if err == nil { err = innerErr } else { err = fmt.Errorf("%s\n%s", err, innerErr) } } }(providerName) } wg.Wait() if len(failedProviders) > 0 { err = fmt.Errorf("Cannnot register providers: %s. Errors were: %s", strings.Join(failedProviders, ", "), err) } return err } func registerWithSubscription(ctx context.Context, providerName string, client resources.ProvidersClient) error { if _, err := client.Register(ctx, providerName); err != nil { return fmt.Errorf("Cannot register provider %s with Azure Resource Manager: %s.", providerName, err) } return nil } go-azure-helpers-0.16.0/response/000077500000000000000000000000001404150134500166225ustar00rootroot00000000000000go-azure-helpers-0.16.0/response/response.go000066400000000000000000000006461404150134500210150ustar00rootroot00000000000000package response import ( "net/http" ) func WasConflict(resp *http.Response) bool { return responseWasStatusCode(resp, http.StatusConflict) } func WasNotFound(resp *http.Response) bool { return responseWasStatusCode(resp, http.StatusNotFound) } func responseWasStatusCode(resp *http.Response, statusCode int) bool { if r := resp; r != nil { if r.StatusCode == statusCode { return true } } return false } go-azure-helpers-0.16.0/response/response_test.go000066400000000000000000000026661404150134500220600ustar00rootroot00000000000000package response import ( "net/http" "testing" ) func TestConflict_DroppedConnection(t *testing.T) { resp := http.Response{} if WasConflict(&resp) { t.Fatalf("wasConflict should return `false` for a dropped connection") } } func TestConflcit_StatusCodes(t *testing.T) { testCases := []struct { statusCode int expectedResult bool }{ {http.StatusOK, false}, {http.StatusInternalServerError, false}, {http.StatusNotFound, false}, {http.StatusConflict, true}, } for _, test := range testCases { resp := http.Response{ StatusCode: test.statusCode, } result := WasConflict(&resp) if test.expectedResult != result { t.Fatalf("Expected '%+v' for status code '%d' - got '%+v'", test.expectedResult, test.statusCode, result) } } } func TestNotFound_DroppedConnection(t *testing.T) { resp := http.Response{} if WasNotFound(&resp) { t.Fatalf("wasNotFound should return `false` for a dropped connection") } } func TestNotFound_StatusCodes(t *testing.T) { testCases := []struct { statusCode int expectedResult bool }{ {http.StatusOK, false}, {http.StatusInternalServerError, false}, {http.StatusNotFound, true}, } for _, test := range testCases { resp := http.Response{ StatusCode: test.statusCode, } result := WasNotFound(&resp) if test.expectedResult != result { t.Fatalf("Expected '%+v' for status code '%d' - got '%+v'", test.expectedResult, test.statusCode, result) } } } go-azure-helpers-0.16.0/sender/000077500000000000000000000000001404150134500162445ustar00rootroot00000000000000go-azure-helpers-0.16.0/sender/sender.go000066400000000000000000000030721404150134500200550ustar00rootroot00000000000000package sender import ( "log" "net/http" "net/http/httputil" "github.com/Azure/go-autorest/autorest" ) func BuildSender(providerName string) autorest.Sender { return autorest.DecorateSender(&http.Client{ Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, }, }, withRequestLogging(providerName)) } func withRequestLogging(providerName string) autorest.SendDecorator { return func(s autorest.Sender) autorest.Sender { return autorest.SenderFunc(func(r *http.Request) (*http.Response, error) { // strip the authorization header prior to printing authHeaderName := "Authorization" auth := r.Header.Get(authHeaderName) if auth != "" { r.Header.Del(authHeaderName) } // dump request to wire format if dump, err := httputil.DumpRequestOut(r, true); err == nil { log.Printf("[DEBUG] %s Request: \n%s\n", providerName, dump) } else { // fallback to basic message log.Printf("[DEBUG] %s Request: %s to %s\n", providerName, r.Method, r.URL) } // add the auth header back if auth != "" { r.Header.Add(authHeaderName, auth) } resp, err := s.Do(r) if resp != nil { // dump response to wire format if dump, err2 := httputil.DumpResponse(resp, true); err2 == nil { log.Printf("[DEBUG] %s Response for %s: \n%s\n", providerName, r.URL, dump) } else { // fallback to basic message log.Printf("[DEBUG] %s Response: %s for %s\n", providerName, resp.Status, r.URL) } } else { log.Printf("[DEBUG] Request to %s completed with no response", r.URL) } return resp, err }) } } go-azure-helpers-0.16.0/storage/000077500000000000000000000000001404150134500164305ustar00rootroot00000000000000go-azure-helpers-0.16.0/storage/sas_token.go000066400000000000000000000155411404150134500207530ustar00rootroot00000000000000package storage import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "fmt" "net/url" "strings" "github.com/Azure/go-autorest/autorest/azure" ) const ( connStringAccountKeyKey = "AccountKey" connStringAccountNameKey = "AccountName" blobContainerSignedVersion = "2018-11-09" ) // ComputeAccountSASToken computes the SAS Token for a Storage Account based on the // access key & given permissions func ComputeAccountSASToken(accountName string, accountKey string, permissions string, services string, resourceTypes string, start string, expiry string, signedProtocol string, signedIp string, // nolint: unparam signedVersion string, // nolint: unparam ) (string, error) { // UTF-8 by default... stringToSign := accountName + "\n" stringToSign += permissions + "\n" stringToSign += services + "\n" stringToSign += resourceTypes + "\n" stringToSign += start + "\n" stringToSign += expiry + "\n" stringToSign += signedIp + "\n" stringToSign += signedProtocol + "\n" stringToSign += signedVersion + "\n" binaryKey, err := base64.StdEncoding.DecodeString(accountKey) if err != nil { return "", err } hasher := hmac.New(sha256.New, binaryKey) hasher.Write([]byte(stringToSign)) signature := hasher.Sum(nil) // Trial and error to determine which fields the Azure portal // URL encodes for a query string and which it does not. sasToken := "?sv=" + url.QueryEscape(signedVersion) sasToken += "&ss=" + url.QueryEscape(services) sasToken += "&srt=" + url.QueryEscape(resourceTypes) sasToken += "&sp=" + url.QueryEscape(permissions) sasToken += "&se=" + (expiry) sasToken += "&st=" + (start) sasToken += "&spr=" + (signedProtocol) // this is consistent with how the Azure portal builds these. if len(signedIp) > 0 { sasToken += "&sip=" + signedIp } sasToken += "&sig=" + url.QueryEscape(base64.StdEncoding.EncodeToString(signature)) return sasToken, nil } // ComputeAccountSASConnectionString computes the composed SAS Connection String for a Storage Account based on the // sas token func ComputeAccountSASConnectionString(env *azure.Environment, accountName string, sasToken string) string { return fmt.Sprintf( "BlobEndpoint=https://%[1]s.blob.%[2]s/;"+ "FileEndpoint=https://%[1]s.file.%[2]s/;"+ "QueueEndpoint=https://%[1]s.queue.%[2]s/;"+ "TableEndpoint=https://%[1]s.table.%[2]s/;"+ "SharedAccessSignature=%[3]s", accountName, env.StorageEndpointSuffix, sasToken[1:]) // need to cut the first character '?' from the sas token } // ComputeAccountSASConnectionUrlForType computes the SAS Connection String for a Storage Account based on the // sas token and the storage type func ComputeAccountSASConnectionUrlForType(env *azure.Environment, accountName string, sasToken string, storageType string) (*string, error) { if !strings.EqualFold(storageType, "blob") && !strings.EqualFold(storageType, "file") && !strings.EqualFold(storageType, "queue") && !strings.EqualFold(storageType, "table") { return nil, fmt.Errorf("Unexpected storage type %s!", storageType) } url := fmt.Sprintf("https://%s.%s.%s%s", accountName, strings.ToLower(storageType), env.StorageEndpointSuffix, sasToken) return &url, nil } func ComputeContainerSASToken(signedPermissions string, signedStart string, signedExpiry string, accountName string, accountKey string, containerName string, signedIdentifier string, signedIp string, signedProtocol string, signedSnapshotTime string, cacheControl string, contentDisposition string, contentEncoding string, contentLanguage string, contentType string, ) (string, error) { canonicalizedResource := "/blob/" + accountName + "/" + containerName signedVersion := blobContainerSignedVersion signedResource := "c" // c for container // UTF-8 by default... stringToSign := signedPermissions + "\n" stringToSign += signedStart + "\n" stringToSign += signedExpiry + "\n" stringToSign += canonicalizedResource + "\n" stringToSign += signedIdentifier + "\n" stringToSign += signedIp + "\n" stringToSign += signedProtocol + "\n" stringToSign += signedVersion + "\n" stringToSign += signedResource + "\n" stringToSign += signedSnapshotTime + "\n" stringToSign += cacheControl + "\n" stringToSign += contentDisposition + "\n" stringToSign += contentEncoding + "\n" stringToSign += contentLanguage + "\n" stringToSign += contentType binaryKey, err := base64.StdEncoding.DecodeString(accountKey) if err != nil { return "", err } hasher := hmac.New(sha256.New, binaryKey) hasher.Write([]byte(stringToSign)) signature := hasher.Sum(nil) sasToken := "?sv=" + signedVersion sasToken += "&sr=" + signedResource sasToken += "&st=" + url.QueryEscape(signedStart) sasToken += "&se=" + url.QueryEscape(signedExpiry) sasToken += "&sp=" + signedPermissions if len(signedIp) > 0 { sasToken += "&sip=" + signedIp } if len(signedProtocol) > 0 { sasToken += "&spr=" + signedProtocol } if len(signedIdentifier) > 0 { sasToken += "&si=" + signedIdentifier } if len(cacheControl) > 0 { sasToken += "&rscc=" + url.QueryEscape(cacheControl) } if len(contentDisposition) > 0 { sasToken += "&rscd=" + url.QueryEscape(contentDisposition) } if len(contentEncoding) > 0 { sasToken += "&rsce=" + url.QueryEscape(contentEncoding) } if len(contentLanguage) > 0 { sasToken += "&rscl=" + url.QueryEscape(contentLanguage) } if len(contentType) > 0 { sasToken += "&rsct=" + url.QueryEscape(contentType) } sasToken += "&sig=" + url.QueryEscape(base64.StdEncoding.EncodeToString(signature)) return sasToken, nil } // ParseAccountSASConnectionString parses the Connection String for a Storage Account func ParseAccountSASConnectionString(connString string) (map[string]string, error) { // This connection string was for a real storage account which has been deleted // so its safe to include here for reference to understand the format. // DefaultEndpointsProtocol=https;AccountName=azurermtestsa0;AccountKey=2vJrjEyL4re2nxCEg590wJUUC7PiqqrDHjAN5RU304FNUQieiEwS2bfp83O0v28iSfWjvYhkGmjYQAdd9x+6nw==;EndpointSuffix=core.windows.net validKeys := map[string]bool{"DefaultEndpointsProtocol": true, "BlobEndpoint": true, "AccountName": true, "AccountKey": true, "EndpointSuffix": true} // The k-v pairs are separated with semi-colons tokens := strings.Split(connString, ";") kvp := make(map[string]string) for _, atoken := range tokens { // The individual k-v are separated by an equals sign. kv := strings.SplitN(atoken, "=", 2) if len(kv) != 2 { return nil, fmt.Errorf("[ERROR] token `%s` is an invalid key=pair (connection string %s)", atoken, connString) } key := kv[0] val := kv[1] if _, present := validKeys[key]; !present { return nil, fmt.Errorf("[ERROR] Unknown Key `%s` in connection string %s", key, connString) } kvp[key] = val } if _, present := kvp[connStringAccountKeyKey]; !present { return nil, fmt.Errorf("[ERROR] Storage Account Key not found in connection string: %s", connString) } return kvp, nil } go-azure-helpers-0.16.0/storage/sas_token_test.go000066400000000000000000000335371404150134500220170ustar00rootroot00000000000000package storage import ( "net/url" "strings" "testing" "github.com/Azure/go-autorest/autorest/azure" ) func TestParseStorageAccountConnectionString(t *testing.T) { testCases := []struct { input string expectedAccountName string expectedAccountKey string expectedError bool }{ { "DefaultEndpointsProtocol=https;AccountName=azurermtestsa0;AccountKey=2vJrjEyL4re2nxCEg590wJUUC7PiqqrDHjAN5RU304FNUQieiEwS2bfp83O0v28iSfWjvYhkGmjYQAdd9x+6nw==;EndpointSuffix=core.windows.net", "azurermtestsa0", "2vJrjEyL4re2nxCEg590wJUUC7PiqqrDHjAN5RU304FNUQieiEwS2bfp83O0v28iSfWjvYhkGmjYQAdd9x+6nw==", false, }, { "DefaultEndpointsProtocol=https;AccountName=azurermtestsa0;AccountKey=2vJrjEyL4re2nxCEg590wJUUC7PiqqrDHjAN5RU304FNUQieiEwS2bfp83O0v28iSfWjvYhkGmjYQAdd9x+6nw==;EndpointSuffix", "", "", true, }, } for _, test := range testCases { result, err := ParseAccountSASConnectionString(test.input) if test.expectedError { if err == nil { t.Fatalf("Expected error for %s: %q", test.input, err) } return } if !test.expectedError && err != nil { t.Fatalf("Failed to parse resource type string: %s, %q", test.input, result) } if val, pres := result[connStringAccountKeyKey]; !pres || val != test.expectedAccountKey { t.Fatalf("Failed to parse Account Key: Expected: %s, Found: %s", test.expectedAccountKey, val) } if val, pres := result[connStringAccountNameKey]; !pres || val != test.expectedAccountName { t.Fatalf("Failed to parse Account Name: Expected: %s, Found: %s", test.expectedAccountName, val) } } } // This connection string was for a real storage account which has been deleted // so its safe to include here for reference to understand the format. // DefaultEndpointsProtocol=https;AccountName=azurermtestsa0;AccountKey=T0ZQouXBDpWud/PlTRHIJH2+VUK8D+fnedEynb9Mx638IYnsMUe4mv1fFjC7t0NayTfFAQJzPZuV1WHFKOzGdg==;EndpointSuffix=core.windows.net func TestComputeAccountSASToken(t *testing.T) { testCases := []struct { accountName string accountKey string permissions string services string resourceTypes string start string expiry string signedProtocol string signedIp string signedVersion string knownSasToken string }{ { "azurermtestsa0", "T0ZQouXBDpWud/PlTRHIJH2+VUK8D+fnedEynb9Mx638IYnsMUe4mv1fFjC7t0NayTfFAQJzPZuV1WHFKOzGdg==", "rwac", "b", "c", "2018-03-20T04:00:00Z", "2020-03-20T04:00:00Z", "https", "", "2017-07-29", "?sv=2017-07-29&ss=b&srt=c&sp=rwac&se=2020-03-20T04:00:00Z&st=2018-03-20T04:00:00Z&spr=https&sig=SQigK%2FnFA4pv0F0oMLqr6DxUWV4vtFqWi6q3Mf7o9nY%3D", }, { "azurermtestsa0", "2vJrjEyL4re2nxCEg590wJUUC7PiqqrDHjAN5RU304FNUQieiEwS2bfp83O0v28iSfWjvYhkGmjYQAdd9x+6nw==", "rwdlac", "b", "sco", "2018-03-20T04:00:00Z", "2018-03-28T05:04:25Z", "https,http", "", "2017-07-29", "?sv=2017-07-29&ss=b&srt=sco&sp=rwdlac&se=2018-03-28T05:04:25Z&st=2018-03-20T04:00:00Z&spr=https,http&sig=OLNwL%2B7gxeDQQaUyNdXcDPK2aCbCMgEkJNjha9te448%3D", }, } for _, test := range testCases { computedToken, err := ComputeAccountSASToken(test.accountName, test.accountKey, test.permissions, test.services, test.resourceTypes, test.start, test.expiry, test.signedProtocol, test.signedIp, test.signedVersion) if err != nil { t.Fatalf("Test Failed: Error computing storage account Sas: %q", err) } if computedToken != test.knownSasToken { t.Fatalf("Test failed: Expected Azure SAS %s but was %s", test.knownSasToken, computedToken) } } } func TestComputeContainerSASToken(t *testing.T) { testCases := []struct { signedPermissions string signedStart string signedExpiry string accountName string accountKey string containerName string signedIdentifier string signedIp string signedProtocol string signedSnapshotTime string cacheControl string contentDisposition string contentEncoding string contentLanguage string contentType string knownSasToken string }{ { "rwl", "2019-03-27", "2019-09-21T09:21Z", "azurermblobcontainertest", "y3PNtHAAyjMSRHZ26n/ISyXt1IpXLIqiwAUQ602Un8AJX2JL3MMEbxK7ue45nr9BB0BibegTkQ5rdrgMR5CZkA==", "test-container", "", "", "https", "", "", "", "", "", "", "?st=2019-03-27&se=2019-09-21T09%3A21Z&sp=rwl&spr=https&sv=2018-11-09&sr=c&sig=DnHeyj11jpfGEmdskSIuASIZorLghcjLbKN90n%2B6UO4%3D", }, { "rwl", "2019-03-27", "2019-09-21T09:21Z", "azurermblobcontainertest", "y3PNtHAAyjMSRHZ26n/ISyXt1IpXLIqiwAUQ602Un8AJX2JL3MMEbxK7ue45nr9BB0BibegTkQ5rdrgMR5CZkA==", "test-container", "", "93.23.223.54", "https", "", "no-cache", "attachment", "gzip", "en-US", "text/html; charset=utf-8", "?st=2019-03-27&se=2019-09-21T09%3A21Z&sp=rwl&sip=93.23.223.54&spr=https&sv=2018-11-09&sr=c&rscc=no-cache&rscd=attachment&rsce=gzip&rscl=en-US&rsct=text/html%3B%20charset%3Dutf-8&sig=M2TaUVEGlRVJjNt/c7Eqt2zH6%2BA8dpiLmTXR0ZevEX8%3D", }, } for _, test := range testCases { computedToken, err := ComputeContainerSASToken(test.signedPermissions, test.signedStart, test.signedExpiry, test.accountName, test.accountKey, test.containerName, test.signedIdentifier, test.signedIp, test.signedProtocol, test.signedSnapshotTime, test.cacheControl, test.contentDisposition, test.contentEncoding, test.contentLanguage, test.contentType) if err != nil { t.Fatalf("Test Failed: Error computing blob container Sas: %q", err) } if !compareSASTokens(computedToken, test.knownSasToken) { t.Fatalf("Test failed: Expected Azure SAS %s but was %s", test.knownSasToken, computedToken) } } } func TestComputeAccountSASConnectionString(t *testing.T) { testCases := []struct { env azure.Environment accountName string sasToken string sasConnectionString string }{ { azure.PublicCloud, "testaccount", "?st=2019-03-27&se=2019-09-21T09%3A21Z&sp=rwl&sip=93.23.223.54&spr=https&sv=2018-11-09&sr=c&rscc=no-cache&rscd=attachment&rsce=gzip&rscl=en-US&rsct=text/html%3B%20charset%3Dutf-8&sig=M2TaUVEGlRVJjNt/c7Eqt2zH6%2BA8dpiLmTXR0ZevEX8%3D", "BlobEndpoint=https://testaccount.blob.core.windows.net/;FileEndpoint=https://testaccount.file.core.windows.net/;QueueEndpoint=https://testaccount.queue.core.windows.net/;TableEndpoint=https://testaccount.table.core.windows.net/;SharedAccessSignature=st=2019-03-27&se=2019-09-21T09%3A21Z&sp=rwl&sip=93.23.223.54&spr=https&sv=2018-11-09&sr=c&rscc=no-cache&rscd=attachment&rsce=gzip&rscl=en-US&rsct=text/html%3B%20charset%3Dutf-8&sig=M2TaUVEGlRVJjNt/c7Eqt2zH6%2BA8dpiLmTXR0ZevEX8%3D", }, { azure.ChinaCloud, "testaccount", "?st=2019-03-27&se=2019-09-21T09%3A21Z&sp=rwl&sip=93.23.223.54&spr=https&sv=2018-11-09&sr=c&rscc=no-cache&rscd=attachment&rsce=gzip&rscl=en-US&rsct=text/html%3B%20charset%3Dutf-8&sig=M2TaUVEGlRVJjNt/c7Eqt2zH6%2BA8dpiLmTXR0ZevEX8%3D", "BlobEndpoint=https://testaccount.blob.core.chinacloudapi.cn/;FileEndpoint=https://testaccount.file.core.chinacloudapi.cn/;QueueEndpoint=https://testaccount.queue.core.chinacloudapi.cn/;TableEndpoint=https://testaccount.table.core.chinacloudapi.cn/;SharedAccessSignature=st=2019-03-27&se=2019-09-21T09%3A21Z&sp=rwl&sip=93.23.223.54&spr=https&sv=2018-11-09&sr=c&rscc=no-cache&rscd=attachment&rsce=gzip&rscl=en-US&rsct=text/html%3B%20charset%3Dutf-8&sig=M2TaUVEGlRVJjNt/c7Eqt2zH6%2BA8dpiLmTXR0ZevEX8%3D", }, { azure.GermanCloud, "testaccount", "?st=2019-03-27&se=2019-09-21T09%3A21Z&sp=rwl&sip=93.23.223.54&spr=https&sv=2018-11-09&sr=c&rscc=no-cache&rscd=attachment&rsce=gzip&rscl=en-US&rsct=text/html%3B%20charset%3Dutf-8&sig=M2TaUVEGlRVJjNt/c7Eqt2zH6%2BA8dpiLmTXR0ZevEX8%3D", "BlobEndpoint=https://testaccount.blob.core.cloudapi.de/;FileEndpoint=https://testaccount.file.core.cloudapi.de/;QueueEndpoint=https://testaccount.queue.core.cloudapi.de/;TableEndpoint=https://testaccount.table.core.cloudapi.de/;SharedAccessSignature=st=2019-03-27&se=2019-09-21T09%3A21Z&sp=rwl&sip=93.23.223.54&spr=https&sv=2018-11-09&sr=c&rscc=no-cache&rscd=attachment&rsce=gzip&rscl=en-US&rsct=text/html%3B%20charset%3Dutf-8&sig=M2TaUVEGlRVJjNt/c7Eqt2zH6%2BA8dpiLmTXR0ZevEX8%3D", }, { azure.USGovernmentCloud, "testaccount", "?st=2019-03-27&se=2019-09-21T09%3A21Z&sp=rwl&sip=93.23.223.54&spr=https&sv=2018-11-09&sr=c&rscc=no-cache&rscd=attachment&rsce=gzip&rscl=en-US&rsct=text/html%3B%20charset%3Dutf-8&sig=M2TaUVEGlRVJjNt/c7Eqt2zH6%2BA8dpiLmTXR0ZevEX8%3D", "BlobEndpoint=https://testaccount.blob.core.usgovcloudapi.net/;FileEndpoint=https://testaccount.file.core.usgovcloudapi.net/;QueueEndpoint=https://testaccount.queue.core.usgovcloudapi.net/;TableEndpoint=https://testaccount.table.core.usgovcloudapi.net/;SharedAccessSignature=st=2019-03-27&se=2019-09-21T09%3A21Z&sp=rwl&sip=93.23.223.54&spr=https&sv=2018-11-09&sr=c&rscc=no-cache&rscd=attachment&rsce=gzip&rscl=en-US&rsct=text/html%3B%20charset%3Dutf-8&sig=M2TaUVEGlRVJjNt/c7Eqt2zH6%2BA8dpiLmTXR0ZevEX8%3D", }, } for _, test := range testCases { computedConnectionString := ComputeAccountSASConnectionString(&test.env, test.accountName, test.sasToken) if computedConnectionString != test.sasConnectionString { t.Fatalf("Test failed: Expected SAS connection string is %s but was %s", computedConnectionString, test.sasConnectionString) } } } func TestComputeAccountSASConnectionUrlForType(t *testing.T) { testCases := []struct { env azure.Environment accountName string sasToken string storageType string storageConnectionUrl string }{ { azure.PublicCloud, "testaccount", "?st=2019-03-27&se=2019-09-21T09%3A21Z&sp=rwl&sip=93.23.223.54&spr=https&sv=2018-11-09&sr=c&rscc=no-cache&rscd=attachment&rsce=gzip&rscl=en-US&rsct=text/html%3B%20charset%3Dutf-8&sig=M2TaUVEGlRVJjNt/c7Eqt2zH6%2BA8dpiLmTXR0ZevEX8%3D", "blob", "https://testaccount.blob.core.windows.net?st=2019-03-27&se=2019-09-21T09%3A21Z&sp=rwl&sip=93.23.223.54&spr=https&sv=2018-11-09&sr=c&rscc=no-cache&rscd=attachment&rsce=gzip&rscl=en-US&rsct=text/html%3B%20charset%3Dutf-8&sig=M2TaUVEGlRVJjNt/c7Eqt2zH6%2BA8dpiLmTXR0ZevEX8%3D", }, { azure.ChinaCloud, "testaccount", "?st=2019-03-27&se=2019-09-21T09%3A21Z&sp=rwl&sip=93.23.223.54&spr=https&sv=2018-11-09&sr=c&rscc=no-cache&rscd=attachment&rsce=gzip&rscl=en-US&rsct=text/html%3B%20charset%3Dutf-8&sig=M2TaUVEGlRVJjNt/c7Eqt2zH6%2BA8dpiLmTXR0ZevEX8%3D", "file", "https://testaccount.file.core.chinacloudapi.cn?st=2019-03-27&se=2019-09-21T09%3A21Z&sp=rwl&sip=93.23.223.54&spr=https&sv=2018-11-09&sr=c&rscc=no-cache&rscd=attachment&rsce=gzip&rscl=en-US&rsct=text/html%3B%20charset%3Dutf-8&sig=M2TaUVEGlRVJjNt/c7Eqt2zH6%2BA8dpiLmTXR0ZevEX8%3D", }, { azure.GermanCloud, "testaccount", "?st=2019-03-27&se=2019-09-21T09%3A21Z&sp=rwl&sip=93.23.223.54&spr=https&sv=2018-11-09&sr=c&rscc=no-cache&rscd=attachment&rsce=gzip&rscl=en-US&rsct=text/html%3B%20charset%3Dutf-8&sig=M2TaUVEGlRVJjNt/c7Eqt2zH6%2BA8dpiLmTXR0ZevEX8%3D", "queue", "https://testaccount.queue.core.cloudapi.de?st=2019-03-27&se=2019-09-21T09%3A21Z&sp=rwl&sip=93.23.223.54&spr=https&sv=2018-11-09&sr=c&rscc=no-cache&rscd=attachment&rsce=gzip&rscl=en-US&rsct=text/html%3B%20charset%3Dutf-8&sig=M2TaUVEGlRVJjNt/c7Eqt2zH6%2BA8dpiLmTXR0ZevEX8%3D", }, { azure.USGovernmentCloud, "testaccount", "?st=2019-03-27&se=2019-09-21T09%3A21Z&sp=rwl&sip=93.23.223.54&spr=https&sv=2018-11-09&sr=c&rscc=no-cache&rscd=attachment&rsce=gzip&rscl=en-US&rsct=text/html%3B%20charset%3Dutf-8&sig=M2TaUVEGlRVJjNt/c7Eqt2zH6%2BA8dpiLmTXR0ZevEX8%3D", "table", "https://testaccount.table.core.usgovcloudapi.net?st=2019-03-27&se=2019-09-21T09%3A21Z&sp=rwl&sip=93.23.223.54&spr=https&sv=2018-11-09&sr=c&rscc=no-cache&rscd=attachment&rsce=gzip&rscl=en-US&rsct=text/html%3B%20charset%3Dutf-8&sig=M2TaUVEGlRVJjNt/c7Eqt2zH6%2BA8dpiLmTXR0ZevEX8%3D", }, { azure.PublicCloud, "testaccount", "?st=2019-03-27&se=2019-09-21T09%3A21Z&sp=rwl&sip=93.23.223.54&spr=https&sv=2018-11-09&sr=c&rscc=no-cache&rscd=attachment&rsce=gzip&rscl=en-US&rsct=text/html%3B%20charset%3Dutf-8&sig=M2TaUVEGlRVJjNt/c7Eqt2zH6%2BA8dpiLmTXR0ZevEX8%3D", "unexpected", "", }, } for _, test := range testCases { computedStorageConnectionUrl, err := ComputeAccountSASConnectionUrlForType(&test.env, test.accountName, test.sasToken, test.storageType) if strings.Compare("unexpected", test.storageType) == 0 { if err == nil { t.Fatalf("Test failed: This call should have thrown an error because an unexpected storage type was specified.") } } else if err != nil { t.Fatalf("Test failed: This call should not have thrown an error") } else if strings.Compare(*computedStorageConnectionUrl, test.storageConnectionUrl) != 0 { t.Fatalf("Test failed: Expected connection url is %s but was %s", *computedStorageConnectionUrl, test.storageConnectionUrl) } } } func compareSASTokens(token1 string, token2 string) bool { queryParams1 := parseSASToken(token1) queryParams2 := parseSASToken(token2) if len(queryParams1) != len(queryParams2) { return false } for k, v1 := range queryParams1 { if v2, ok := queryParams2[k]; ok { // values need to be unescaped because apperently azure cli escape does not seem to work correctly (e.g. for contentType value) // example: text/html; charset=utf-8 // -> escaped in go: text%2Fhtml%3B+charset%3Dutf-8 // -> escaped in python / azure cli: text/html%3B%20charset%3Dutf-8 unescapedValue1, _ := url.QueryUnescape(v1) unescapedValue2, _ := url.QueryUnescape(v2) if unescapedValue1 != unescapedValue2 { return false } } else { return false } } return true } func parseSASToken(token string) map[string]string { queryParts := strings.Split(token[1:], "&") kvp := make(map[string]string) for _, queryPart := range queryParts { kv := strings.SplitN(queryPart, "=", 2) key := kv[0] value := kv[1] kvp[key] = value } return kvp }