From 0e745398c3cf95eb4af79c8cfb995a64cecc0ae5 Mon Sep 17 00:00:00 2001 From: Joel Hendrix Date: Fri, 15 Sep 2023 11:20:26 -0700 Subject: [PATCH] Add KeyCredential and SASCredential types (#21553) * Add KeyCredential and SASCredential types Includes supporting pipeline policies, config options, etc. * add tests * change Format func to Prefix string * remove error return value from constructors * remove error return for Update method --- sdk/azcore/CHANGELOG.md | 5 ++ sdk/azcore/core.go | 18 ++++++ sdk/azcore/core_test.go | 8 +++ sdk/azcore/internal/exported/exported.go | 63 +++++++++++++++++++ sdk/azcore/internal/exported/exported_test.go | 41 ++++++++++++ sdk/azcore/runtime/policy_key_credential.go | 49 +++++++++++++++ .../runtime/policy_key_credential_test.go | 50 +++++++++++++++ sdk/azcore/runtime/policy_sas_credential.go | 39 ++++++++++++ .../runtime/polilcy_sas_credential_test.go | 34 ++++++++++ 9 files changed, 307 insertions(+) create mode 100644 sdk/azcore/runtime/policy_key_credential.go create mode 100644 sdk/azcore/runtime/policy_key_credential_test.go create mode 100644 sdk/azcore/runtime/policy_sas_credential.go create mode 100644 sdk/azcore/runtime/polilcy_sas_credential_test.go diff --git a/sdk/azcore/CHANGELOG.md b/sdk/azcore/CHANGELOG.md index 8682b7ee4af8..0e0168d42bcf 100644 --- a/sdk/azcore/CHANGELOG.md +++ b/sdk/azcore/CHANGELOG.md @@ -4,6 +4,11 @@ ### Features Added +* Added types `KeyCredential` and `SASCredential` to the `azcore` package. + * Includes their respective constructor functions. +* Added types `KeyCredentialPolicy` and `SASCredentialPolicy` to the `azcore/runtime` package. + * Includes their respective constructor functions and options types. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/azcore/core.go b/sdk/azcore/core.go index e10b1ea093b3..d2172ced5811 100644 --- a/sdk/azcore/core.go +++ b/sdk/azcore/core.go @@ -22,6 +22,24 @@ type AccessToken = exported.AccessToken // TokenCredential represents a credential capable of providing an OAuth token. type TokenCredential = exported.TokenCredential +// KeyCredential contains an authentication key used to authenticate to an Azure service. +type KeyCredential = exported.KeyCredential + +// NewKeyCredential creates a new instance of [KeyCredential] with the specified values. +// - key is the authentication key +func NewKeyCredential(key string) *KeyCredential { + return exported.NewKeyCredential(key) +} + +// SASCredential contains a shared access signature used to authenticate to an Azure service. +type SASCredential = exported.SASCredential + +// NewSASCredential creates a new instance of [SASCredential] with the specified values. +// - sas is the shared access signature +func NewSASCredential(sas string) *SASCredential { + return exported.NewSASCredential(sas) +} + // holds sentinel values used to send nulls var nullables map[reflect.Type]interface{} = map[reflect.Type]interface{}{} diff --git a/sdk/azcore/core_test.go b/sdk/azcore/core_test.go index 37812e80a477..43876043ed2e 100644 --- a/sdk/azcore/core_test.go +++ b/sdk/azcore/core_test.go @@ -221,3 +221,11 @@ func TestClientWithClientName(t *testing.T) { require.NoError(t, err) require.EqualValues(t, "az.namespace:Widget.Factory", attrString) } + +func TestNewKeyCredential(t *testing.T) { + require.NotNil(t, NewKeyCredential("foo")) +} + +func TestNewSASCredential(t *testing.T) { + require.NotNil(t, NewSASCredential("foo")) +} diff --git a/sdk/azcore/internal/exported/exported.go b/sdk/azcore/internal/exported/exported.go index 132f2a474fb9..f2b296b6dc7c 100644 --- a/sdk/azcore/internal/exported/exported.go +++ b/sdk/azcore/internal/exported/exported.go @@ -12,6 +12,7 @@ import ( "fmt" "io" "net/http" + "sync/atomic" "time" ) @@ -110,3 +111,65 @@ func DecodeByteArray(s string, v *[]byte, format Base64Encoding) error { return fmt.Errorf("unrecognized byte array format: %d", format) } } + +// KeyCredential contains an authentication key used to authenticate to an Azure service. +// Exported as azcore.KeyCredential. +type KeyCredential struct { + cred *keyCredential +} + +// NewKeyCredential creates a new instance of [KeyCredential] with the specified values. +// - key is the authentication key +func NewKeyCredential(key string) *KeyCredential { + return &KeyCredential{cred: newKeyCredential(key)} +} + +// Update replaces the existing key with the specified value. +func (k *KeyCredential) Update(key string) { + k.cred.Update(key) +} + +// SASCredential contains a shared access signature used to authenticate to an Azure service. +// Exported as azcore.SASCredential. +type SASCredential struct { + cred *keyCredential +} + +// NewSASCredential creates a new instance of [SASCredential] with the specified values. +// - sas is the shared access signature +func NewSASCredential(sas string) *SASCredential { + return &SASCredential{cred: newKeyCredential(sas)} +} + +// Update replaces the existing shared access signature with the specified value. +func (k *SASCredential) Update(sas string) { + k.cred.Update(sas) +} + +// KeyCredentialGet returns the key for cred. +func KeyCredentialGet(cred *KeyCredential) string { + return cred.cred.Get() +} + +// SASCredentialGet returns the shared access sig for cred. +func SASCredentialGet(cred *SASCredential) string { + return cred.cred.Get() +} + +type keyCredential struct { + key atomic.Value // string +} + +func newKeyCredential(key string) *keyCredential { + keyCred := keyCredential{} + keyCred.key.Store(key) + return &keyCred +} + +func (k *keyCredential) Get() string { + return k.key.Load().(string) +} + +func (k *keyCredential) Update(key string) { + k.key.Store(key) +} diff --git a/sdk/azcore/internal/exported/exported_test.go b/sdk/azcore/internal/exported/exported_test.go index 6b0776f64683..5132d1d697ab 100644 --- a/sdk/azcore/internal/exported/exported_test.go +++ b/sdk/azcore/internal/exported/exported_test.go @@ -7,9 +7,12 @@ package exported import ( + "fmt" "net/http" "strings" "testing" + + "github.com/stretchr/testify/require" ) func TestNopCloser(t *testing.T) { @@ -33,3 +36,41 @@ func TestHasStatusCode(t *testing.T) { t.Fatal("unexpected failure") } } + +func TestDecodeByteArray(t *testing.T) { + out := []byte{} + require.NoError(t, DecodeByteArray("", &out, Base64StdFormat)) + require.Empty(t, out) + const ( + stdEncoding = "VGVzdERlY29kZUJ5dGVBcnJheQ==" + urlEncoding = "VGVzdERlY29kZUJ5dGVBcnJheQ" + decoded = "TestDecodeByteArray" + ) + require.NoError(t, DecodeByteArray(stdEncoding, &out, Base64StdFormat)) + require.EqualValues(t, decoded, string(out)) + require.NoError(t, DecodeByteArray(urlEncoding, &out, Base64URLFormat)) + require.EqualValues(t, decoded, string(out)) + require.NoError(t, DecodeByteArray(fmt.Sprintf("\"%s\"", stdEncoding), &out, Base64StdFormat)) + require.EqualValues(t, decoded, string(out)) + require.Error(t, DecodeByteArray(stdEncoding, &out, 123)) +} + +func TestNewKeyCredential(t *testing.T) { + const val1 = "foo" + cred := NewKeyCredential(val1) + require.NotNil(t, cred) + require.EqualValues(t, val1, KeyCredentialGet(cred)) + const val2 = "bar" + cred.Update(val2) + require.EqualValues(t, val2, KeyCredentialGet(cred)) +} + +func TestNewSASCredential(t *testing.T) { + const val1 = "foo" + cred := NewSASCredential(val1) + require.NotNil(t, cred) + require.EqualValues(t, val1, SASCredentialGet(cred)) + const val2 = "bar" + cred.Update(val2) + require.EqualValues(t, val2, SASCredentialGet(cred)) +} diff --git a/sdk/azcore/runtime/policy_key_credential.go b/sdk/azcore/runtime/policy_key_credential.go new file mode 100644 index 000000000000..2e47a5bad065 --- /dev/null +++ b/sdk/azcore/runtime/policy_key_credential.go @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package runtime + +import ( + "net/http" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" +) + +// KeyCredentialPolicy authorizes requests with a [azcore.KeyCredential]. +type KeyCredentialPolicy struct { + cred *exported.KeyCredential + header string + prefix string +} + +// KeyCredentialPolicyOptions contains the optional values configuring [KeyCredentialPolicy]. +type KeyCredentialPolicyOptions struct { + // Prefix is used if the key requires a prefix before it's inserted into the HTTP request. + Prefix string +} + +// NewKeyCredentialPolicy creates a new instance of [KeyCredentialPolicy]. +// - cred is the [azcore.KeyCredential] used to authenticate with the service +// - header is the name of the HTTP request header in which the key is placed +// - options contains optional configuration, pass nil to accept the default values +func NewKeyCredentialPolicy(cred *exported.KeyCredential, header string, options *KeyCredentialPolicyOptions) *KeyCredentialPolicy { + if options == nil { + options = &KeyCredentialPolicyOptions{} + } + return &KeyCredentialPolicy{ + cred: cred, + header: header, + prefix: options.Prefix, + } +} + +// Do implementes the Do method on the [policy.Polilcy] interface. +func (k *KeyCredentialPolicy) Do(req *policy.Request) (*http.Response, error) { + val := exported.KeyCredentialGet(k.cred) + if k.prefix != "" { + val = k.prefix + val + } + req.Raw().Header.Add(k.header, val) + return req.Next() +} diff --git a/sdk/azcore/runtime/policy_key_credential_test.go b/sdk/azcore/runtime/policy_key_credential_test.go new file mode 100644 index 000000000000..e26e009ff602 --- /dev/null +++ b/sdk/azcore/runtime/policy_key_credential_test.go @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package runtime + +import ( + "context" + "net/http" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared" + "github.com/stretchr/testify/require" +) + +func TestKeyCredentialPolicy(t *testing.T) { + const key = "foo" + cred := exported.NewKeyCredential(key) + + const headerName = "fake-auth" + policy := NewKeyCredentialPolicy(cred, headerName, nil) + require.NotNil(t, policy) + + pl := exported.NewPipeline(shared.TransportFunc(func(req *http.Request) (*http.Response, error) { + require.EqualValues(t, key, req.Header.Get(headerName)) + return &http.Response{}, nil + }), policy) + + req, err := NewRequest(context.Background(), http.MethodGet, "http://contoso.com") + require.NoError(t, err) + + _, err = pl.Do(req) + require.NoError(t, err) + + policy = NewKeyCredentialPolicy(cred, headerName, &KeyCredentialPolicyOptions{ + Prefix: "Prefix: ", + }) + require.NotNil(t, policy) + + pl = exported.NewPipeline(shared.TransportFunc(func(req *http.Request) (*http.Response, error) { + require.EqualValues(t, "Prefix: "+key, req.Header.Get(headerName)) + return &http.Response{}, nil + }), policy) + + req, err = NewRequest(context.Background(), http.MethodGet, "http://contoso.com") + require.NoError(t, err) + + _, err = pl.Do(req) + require.NoError(t, err) +} diff --git a/sdk/azcore/runtime/policy_sas_credential.go b/sdk/azcore/runtime/policy_sas_credential.go new file mode 100644 index 000000000000..25266030ba21 --- /dev/null +++ b/sdk/azcore/runtime/policy_sas_credential.go @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package runtime + +import ( + "net/http" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" +) + +// SASCredentialPolicy authorizes requests with a [azcore.SASCredential]. +type SASCredentialPolicy struct { + cred *exported.SASCredential + header string +} + +// SASCredentialPolicyOptions contains the optional values configuring [SASCredentialPolicy]. +type SASCredentialPolicyOptions struct { + // placeholder for future optional values +} + +// NewSASCredentialPolicy creates a new instance of [SASCredentialPolicy]. +// - cred is the [azcore.SASCredential] used to authenticate with the service +// - header is the name of the HTTP request header in which the shared access signature is placed +// - options contains optional configuration, pass nil to accept the default values +func NewSASCredentialPolicy(cred *exported.SASCredential, header string, options *SASCredentialPolicyOptions) *SASCredentialPolicy { + return &SASCredentialPolicy{ + cred: cred, + header: header, + } +} + +// Do implementes the Do method on the [policy.Polilcy] interface. +func (k *SASCredentialPolicy) Do(req *policy.Request) (*http.Response, error) { + req.Raw().Header.Add(k.header, exported.SASCredentialGet(k.cred)) + return req.Next() +} diff --git a/sdk/azcore/runtime/polilcy_sas_credential_test.go b/sdk/azcore/runtime/polilcy_sas_credential_test.go new file mode 100644 index 000000000000..5853eed253d7 --- /dev/null +++ b/sdk/azcore/runtime/polilcy_sas_credential_test.go @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package runtime + +import ( + "context" + "net/http" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared" + "github.com/stretchr/testify/require" +) + +func TestSASCredentialPolicy(t *testing.T) { + const key = "foo" + cred := exported.NewSASCredential(key) + + const headerName = "fake-auth" + policy := NewSASCredentialPolicy(cred, headerName, nil) + require.NotNil(t, policy) + + pl := exported.NewPipeline(shared.TransportFunc(func(req *http.Request) (*http.Response, error) { + require.EqualValues(t, key, req.Header.Get(headerName)) + return &http.Response{}, nil + }), policy) + + req, err := NewRequest(context.Background(), http.MethodGet, "http://contoso.com") + require.NoError(t, err) + + _, err = pl.Do(req) + require.NoError(t, err) +}