diff --git a/sdk/security/keyvault/internal/CHANGELOG.md b/sdk/security/keyvault/internal/CHANGELOG.md new file mode 100644 index 000000000000..777d18d4f095 --- /dev/null +++ b/sdk/security/keyvault/internal/CHANGELOG.md @@ -0,0 +1,66 @@ +# Release History + +## 0.8.0 (Unreleased) + +### Features Added + +### Breaking Changes +* Moved to new location + +### Bugs Fixed + +### Other Changes +* Upgrade to `azcore` v1.3.0 + +## 0.7.1 (2022-11-14) + +### Bugs Fixed +* `KeyVaultChallengePolicy` uses incorrect authentication scope when challenge verification is disabled + +## 0.7.0 (2022-09-20) + +### Breaking Changes +* Added `*KeyVaultChallengePolicyOptions` parameter to `NewKeyVaultChallengePolicy` + +## 0.6.0 (2022-09-12) + +### Breaking Changes +* Verify the challenge resource matches the vault domain. See https://aka.ms/azsdk/blog/vault-uri for more information. +* `ParseID()` no longer appends a trailing slash to vault URLs + +## 0.5.0 (2022-05-12) + +### Breaking Changes +* Removed `ExpiringResource` and its dependencies in favor of shared implementation from `internal/temporal`. + +### Other Changes +* Updated to latest versions of `azcore` and `internal`. + +## 0.4.0 (2022-04-22) + +### Breaking Changes +* Updated `ExpiringResource` and its dependent types to use generics. + +### Other Changes +* Remove reference to `TokenRequestOptions.TenantID` as it's been removed and wasn't working anyways. + +## 0.3.0 (2022-04-04) + +### Features Added +* Adds the `ParseKeyvaultID` function to parse an ID into the Key Vault URL, item name, and item version + +### Breaking Changes +* Updates to azcore v0.23.0 + +## 0.2.1 (2022-01-31) + +### Bugs Fixed +* Avoid retries on terminal failures (#16932) + +## 0.2.0 (2022-01-12) + +### Bugs Fixed +* Fixes a bug with Managed HSMs that prevented correctly authorizing requests. + +## 0.1.0 (2021-11-09) +* This is the initial release of the `internal` library for KeyVault diff --git a/sdk/security/keyvault/internal/LICENSE.txt b/sdk/security/keyvault/internal/LICENSE.txt new file mode 100644 index 000000000000..d1ca00f20a89 --- /dev/null +++ b/sdk/security/keyvault/internal/LICENSE.txt @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE \ No newline at end of file diff --git a/sdk/security/keyvault/internal/README.md b/sdk/security/keyvault/internal/README.md new file mode 100644 index 000000000000..8516337cf614 --- /dev/null +++ b/sdk/security/keyvault/internal/README.md @@ -0,0 +1,21 @@ +# Key Vault Internal Module for Go + +This module contains shared code for all the Key Vault SDKs, mainly the challenge authentication policy. + +## Contributing +This project welcomes contributions and suggestions. Most contributions require +you to agree to a Contributor License Agreement (CLA) declaring that you have +the right to, and actually do, grant us the rights to use your contribution. +For details, visit [https://cla.microsoft.com](https://cla.microsoft.com). + +When you submit a pull request, a CLA-bot will automatically determine whether +you need to provide a CLA and decorate the PR appropriately (e.g., label, +comment). Simply follow the instructions provided by the bot. You will only +need to do this once across all repos using our CLA. + +This project has adopted the +[Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information, see the +[Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any +additional questions or comments. diff --git a/sdk/security/keyvault/internal/challenge_policy.go b/sdk/security/keyvault/internal/challenge_policy.go new file mode 100644 index 000000000000..f5c8b725f2f4 --- /dev/null +++ b/sdk/security/keyvault/internal/challenge_policy.go @@ -0,0 +1,175 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package internal + +import ( + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/internal/errorinfo" +) + +const challengeMatchError = `challenge resource "%s" doesn't match the requested domain. Set DisableChallengeResourceVerification to true in your client options to disable. See https://aka.ms/azsdk/blog/vault-uri for more information` + +type KeyVaultChallengePolicyOptions struct { + // DisableChallengeResourceVerification controls whether the policy requires the + // authentication challenge resource to match the Key Vault or Managed HSM domain + DisableChallengeResourceVerification bool +} + +type keyVaultAuthorizer struct { + // tro is the policy's authentication parameters. These are discovered from an authentication challenge + // elicited ahead of the first client request. + tro policy.TokenRequestOptions + // TODO: move into tro once it has a tenant field (https://github.com/Azure/azure-sdk-for-go/issues/19841) + tenantID string + verifyChallengeResource bool +} + +type reqBody struct { + body io.ReadSeekCloser + contentType string +} + +func NewKeyVaultChallengePolicy(cred azcore.TokenCredential, opts *KeyVaultChallengePolicyOptions) policy.Policy { + if opts == nil { + opts = &KeyVaultChallengePolicyOptions{} + } + kv := keyVaultAuthorizer{ + verifyChallengeResource: !opts.DisableChallengeResourceVerification, + } + return runtime.NewBearerTokenPolicy(cred, nil, &policy.BearerTokenOptions{ + AuthorizationHandler: policy.AuthorizationHandler{ + OnRequest: kv.authorize, + OnChallenge: kv.authorizeOnChallenge, + }, + }) +} + +func (k *keyVaultAuthorizer) authorize(req *policy.Request, authNZ func(policy.TokenRequestOptions) error) error { + if len(k.tro.Scopes) == 0 || k.tenantID == "" { + if body := req.Body(); body != nil { + // We don't know the scope or tenant ID because we haven't seen a challenge yet. We elicit one now by sending + // the request without authorization, first removing its body, if any. authorizeOnChallenge will reattach the + // body, authorize the request, and send it again. + rb := reqBody{body, req.Raw().Header.Get("content-type")} + req.SetOperationValue(rb) + if err := req.SetBody(nil, ""); err != nil { + return err + } + } + // returning nil indicates the bearer token policy should send the request + return nil + } + // else we know the auth parameters and can authorize the request as normal + return authNZ(k.tro) +} + +func (k *keyVaultAuthorizer) authorizeOnChallenge(req *policy.Request, res *http.Response, authNZ func(policy.TokenRequestOptions) error) error { + // parse the challenge + if err := k.updateTokenRequestOptions(res, req.Raw()); err != nil { + return err + } + // reattach the request's original body, if it was removed by authorize(). If a bug prevents recovering + // the body, this policy will send the request without it and get a 400 response from Key Vault. + var rb reqBody + if req.OperationValue(&rb) { + if err := req.SetBody(rb.body, rb.contentType); err != nil { + return err + } + } + // authenticate with the parameters supplied by Key Vault, authorize the request, send it again + return authNZ(k.tro) +} + +// parses Tenant ID from auth challenge +// https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000 +func parseTenant(url string) string { + if url == "" { + return "" + } + parts := strings.Split(url, "/") + tenant := parts[3] + tenant = strings.ReplaceAll(tenant, ",", "") + return tenant +} + +type challengePolicyError struct { + err error +} + +func (c *challengePolicyError) Error() string { + return c.err.Error() +} + +func (*challengePolicyError) NonRetriable() { + // marker method +} + +func (c *challengePolicyError) Unwrap() error { + return c.err +} + +var _ errorinfo.NonRetriable = (*challengePolicyError)(nil) + +// updateTokenRequestOptions parses authentication parameters from Key Vault's challenge +func (k *keyVaultAuthorizer) updateTokenRequestOptions(resp *http.Response, req *http.Request) error { + authHeader := resp.Header.Get("WWW-Authenticate") + if authHeader == "" { + return &challengePolicyError{err: errors.New("response has no WWW-Authenticate header for challenge authentication")} + } + + // Strip down to auth and resource + // Format is "Bearer authorization=\"\" resource=\"\"" OR + // "Bearer authorization=\"\" scope=\"\" resource=\"\"" + authHeader = strings.ReplaceAll(authHeader, "Bearer ", "") + + parts := strings.Split(authHeader, " ") + + vals := map[string]string{} + for _, part := range parts { + subParts := strings.Split(part, "=") + if len(subParts) == 2 { + stripped := strings.ReplaceAll(subParts[1], "\"", "") + stripped = strings.TrimSuffix(stripped, ",") + vals[subParts[0]] = stripped + } + } + + k.tenantID = parseTenant(vals["authorization"]) + scope := "" + if v, ok := vals["scope"]; ok { + scope = v + } else if v, ok := vals["resource"]; ok { + scope = v + } + if scope == "" { + return &challengePolicyError{err: errors.New("could not find a valid resource in the WWW-Authenticate header")} + } + if k.verifyChallengeResource { + // the challenge resource's host must match the requested vault's host + parsed, err := url.Parse(scope) + if err != nil { + return &challengePolicyError{err: fmt.Errorf(`invalid challenge resource "%s": %v`, scope, err)} + } + if !strings.HasSuffix(req.URL.Host, "."+parsed.Host) { + return &challengePolicyError{err: fmt.Errorf(challengeMatchError, scope)} + } + } + if !strings.HasSuffix(scope, "/.default") { + scope += "/.default" + } + k.tro.Scopes = []string{scope} + return nil +} diff --git a/sdk/security/keyvault/internal/challenge_policy_test.go b/sdk/security/keyvault/internal/challenge_policy_test.go new file mode 100644 index 000000000000..f9352aa18fed --- /dev/null +++ b/sdk/security/keyvault/internal/challenge_policy_test.go @@ -0,0 +1,109 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package internal + +import ( + "context" + "fmt" + "net/http" + "strings" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/internal/mock" + "github.com/stretchr/testify/require" +) + +type credentialFunc func(context.Context, policy.TokenRequestOptions) (azcore.AccessToken, error) + +func (cf credentialFunc) GetToken(ctx context.Context, options policy.TokenRequestOptions) (azcore.AccessToken, error) { + return cf(ctx, options) +} + +func TestChallengePolicy(t *testing.T) { + accessToken := "***" + resource := "https://vault.azure.net" + scope := "https://vault.azure.net/.default" + challengeResource := `Bearer authorization="https://login.microsoftonline.com/{tenant}", resource="{resource}"` + challengeScope := `Bearer authorization="https://login.microsoftonline.com/{tenant}", scope="{resource}"` + + for _, test := range []struct { + expectedScope, format, resource string + disableVerify, err bool + }{ + // happy path: resource matches requested vault's host (vault.azure.net) + {format: challengeResource, resource: resource, expectedScope: scope}, + {format: challengeResource, resource: resource, disableVerify: true, expectedScope: scope}, + {format: challengeScope, resource: scope, expectedScope: scope}, + {format: challengeScope, resource: scope, disableVerify: true, expectedScope: scope}, + // the policy should prefer scope to resource when a challenge specifies both + {format: fmt.Sprintf(`%s scope="%s"`, challengeResource, scope), resource: resource, expectedScope: scope}, + {format: challengeScope + ` resource="ignore me"`, resource: scope, expectedScope: scope}, + + // error cases: resource/scope doesn't match the requested vault's host (vault.azure.net) + {format: challengeResource, resource: "https://vault.azure.cn", err: true}, + {format: challengeResource, resource: "https://myvault.azure.net", err: true}, + {format: challengeScope, resource: "https://vault.azure.cn/.default", err: true}, + {format: challengeScope, resource: "https://myvault.azure.net/.default", err: true}, + + // the policy shouldn't return errors for the above cases when verification is disabled + {format: challengeResource, resource: "https://vault.azure.cn", disableVerify: true, expectedScope: "https://vault.azure.cn/.default"}, + {format: challengeResource, resource: "https://myvault.azure.net", disableVerify: true, expectedScope: "https://myvault.azure.net/.default"}, + {format: challengeScope, resource: "https://vault.azure.cn/.default", disableVerify: true, expectedScope: "https://vault.azure.cn/.default"}, + {format: challengeScope, resource: "https://myvault.azure.net/.default", disableVerify: true, expectedScope: "https://myvault.azure.net/.default"}, + } { + t.Run("", func(t *testing.T) { + srv, close := mock.NewServer(mock.WithTransformAllRequestsToTestServerUrl()) + defer close() + srv.AppendResponse( + mock.WithHeader("WWW-Authenticate", strings.ReplaceAll(test.format, "{resource}", test.resource)), + mock.WithStatusCode(401), + ) + srv.AppendResponse(mock.WithPredicate(func(r *http.Request) bool { + if authz := r.Header.Values("Authorization"); len(authz) != 1 || authz[0] != "Bearer "+accessToken { + t.Errorf(`unexpected Authorization "%s"`, authz) + } + return true + })) + srv.AppendResponse() + authenticated := false + cred := credentialFunc(func(ctx context.Context, tro policy.TokenRequestOptions) (azcore.AccessToken, error) { + authenticated = true + require.Equal(t, []string{test.expectedScope}, tro.Scopes) + return azcore.AccessToken{Token: accessToken, ExpiresOn: time.Now().Add(time.Hour)}, nil + }) + p := NewKeyVaultChallengePolicy(cred, &KeyVaultChallengePolicyOptions{DisableChallengeResourceVerification: test.disableVerify}) + pl := runtime.NewPipeline("", "", + runtime.PipelineOptions{PerRetry: []policy.Policy{p}}, + &policy.ClientOptions{Transport: srv}, + ) + req, err := runtime.NewRequest(context.Background(), "GET", "https://42.vault.azure.net") + require.NoError(t, err) + _, err = pl.Do(req) + if test.err { + expected := fmt.Sprintf(challengeMatchError, test.resource) + require.EqualError(t, err, expected) + require.IsType(t, &challengePolicyError{}, err) + } else { + require.True(t, authenticated, "policy should have authenticated") + } + }) + } +} + +func TestParseTenant(t *testing.T) { + actual := parseTenant("") + require.Empty(t, actual) + + expected := "00000000-0000-0000-0000-000000000000" + sampleURL := "https://login.microsoftonline.com/" + expected + actual = parseTenant(sampleURL) + require.Equal(t, expected, actual, "tenant was not properly parsed, got %s, expected %s", actual, expected) +} diff --git a/sdk/security/keyvault/internal/ci.securitykeyvault.yml b/sdk/security/keyvault/internal/ci.securitykeyvault.yml new file mode 100644 index 000000000000..2f8b8e1a87ad --- /dev/null +++ b/sdk/security/keyvault/internal/ci.securitykeyvault.yml @@ -0,0 +1,28 @@ +# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file. +trigger: + branches: + include: + - main + - feature/* + - hotfix/* + - release/* + paths: + include: + - sdk/security/keyvault/internal + +pr: + branches: + include: + - main + - feature/* + - hotfix/* + - release/* + paths: + include: + - sdk/security/keyvault/internal + +stages: +- template: /eng/pipelines/templates/jobs/archetype-sdk-client.yml + parameters: + ServiceDirectory: 'security/keyvault/internal' + RunLiveTests: false diff --git a/sdk/security/keyvault/internal/constants.go b/sdk/security/keyvault/internal/constants.go new file mode 100644 index 000000000000..610f1544f363 --- /dev/null +++ b/sdk/security/keyvault/internal/constants.go @@ -0,0 +1,11 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package internal + +const ( + version = "v0.8.0" //nolint +) diff --git a/sdk/security/keyvault/internal/doc.go b/sdk/security/keyvault/internal/doc.go new file mode 100644 index 000000000000..d8f93492f51d --- /dev/null +++ b/sdk/security/keyvault/internal/doc.go @@ -0,0 +1,7 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package internal diff --git a/sdk/security/keyvault/internal/go.mod b/sdk/security/keyvault/internal/go.mod new file mode 100644 index 000000000000..0debbe735836 --- /dev/null +++ b/sdk/security/keyvault/internal/go.mod @@ -0,0 +1,17 @@ +module github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal + +go 1.18 + +require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.3.0 + github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1 + github.com/stretchr/testify v1.7.0 +) + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect + golang.org/x/text v0.3.7 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/sdk/security/keyvault/internal/go.sum b/sdk/security/keyvault/internal/go.sum new file mode 100644 index 000000000000..45bf8a734a06 --- /dev/null +++ b/sdk/security/keyvault/internal/go.sum @@ -0,0 +1,19 @@ +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.3.0 h1:VuHAcMq8pU1IWNT/m5yRaGqbK0BiQKHT8X4DTp9CHdI= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.3.0/go.mod h1:tZoQYdDZNOiIjdSn0dVWVfl0NEPGOJqVLzSrcFk4Is0= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1 h1:Oj853U9kG+RLTCQXpjvOnrv0WaZHxgmZz1TlLywgOPY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/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.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/sdk/security/keyvault/internal/parse.go b/sdk/security/keyvault/internal/parse.go new file mode 100644 index 000000000000..8511832d27c0 --- /dev/null +++ b/sdk/security/keyvault/internal/parse.go @@ -0,0 +1,37 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +package internal + +import ( + "fmt" + "net/url" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" +) + +// ParseID parses "https://myvaultname.vault.azure.net/keys/key1053998307/b86c2e6ad9054f4abf69cc185b99aa60" +// into "https://myvaultname.managedhsm.azure.net/", "key1053998307", and "b86c2e6ad9054f4abf69cc185b99aa60" +func ParseID(id *string) (*string, *string, *string) { + if id == nil { + return nil, nil, nil + } + parsed, err := url.Parse(*id) + if err != nil { + return nil, nil, nil + } + + url := fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host) + split := strings.Split(strings.TrimPrefix(parsed.Path, "/"), "/") + if len(split) < 3 { + if len(split) == 2 { + return &url, to.Ptr(split[1]), nil + } + return &url, nil, nil + } + + return &url, to.Ptr(split[1]), to.Ptr(split[2]) +} diff --git a/sdk/security/keyvault/internal/parse_test.go b/sdk/security/keyvault/internal/parse_test.go new file mode 100644 index 000000000000..6f1fcb1bf62f --- /dev/null +++ b/sdk/security/keyvault/internal/parse_test.go @@ -0,0 +1,46 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +package internal + +import ( + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/stretchr/testify/require" +) + +func TestParseID(t *testing.T) { + examples := map[string]struct{ url, name, version *string }{ + "https://myvaultname.vault.azure.net/keys/key1053998307/b86c2e6ad9054f4abf69cc185b99aa60": {to.Ptr("https://myvaultname.vault.azure.net"), to.Ptr("key1053998307"), to.Ptr("b86c2e6ad9054f4abf69cc185b99aa60")}, + "https://myvaultname.vault.azure.net:8080/keys/key1053998307/b86c2e6ad9054f4abf69cc185b99aa60": {to.Ptr("https://myvaultname.vault.azure.net:8080"), to.Ptr("key1053998307"), to.Ptr("b86c2e6ad9054f4abf69cc185b99aa60")}, + "https://myvaultname.vault.azure.net/keys/key1053998307": {to.Ptr("https://myvaultname.vault.azure.net"), to.Ptr("key1053998307"), nil}, + "https://myvaultname.vault.azure.net:8080/keys/key1053998307": {to.Ptr("https://myvaultname.vault.azure.net:8080"), to.Ptr("key1053998307"), nil}, + "https://myvaultname.vault.azure.net/": {to.Ptr("https://myvaultname.vault.azure.net"), nil, nil}, + "https://myvaultname.vault.azure.net:8080": {to.Ptr("https://myvaultname.vault.azure.net:8080"), nil, nil}, + } + + for url, expected := range examples { + url, name, version := ParseID(&url) + if expected.url == nil { + require.Nil(t, url) + } else { + require.NotNil(t, url) + require.Equal(t, *expected.url, *url) + } + if expected.name == nil { + require.Nil(t, name) + } else { + require.NotNilf(t, name, "expected %s", *expected.name) + require.Equal(t, *expected.name, *name) + } + if expected.version == nil { + require.Nil(t, version) + } else { + require.NotNil(t, version) + require.Equal(t, *expected.version, *version) + } + } +}