Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

storage: switching to use SharedKey for authentication #4235

Merged
merged 11 commits into from
Sep 4, 2019
Merged
111 changes: 111 additions & 0 deletions azurerm/internal/authorizers/authorizer_shared_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package authorizers

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

"github.com/Azure/go-autorest/autorest"
)

// TODO: switch to using the version from github.com/Azure/go-autorest
// once https://github.com/Azure/go-autorest/pull/416 has been merged

// SharedKeyAuthorizer implements an authorization for Shared Key
// this can be used for interaction with Blob, File and Queue Storage Endpoints
type SharedKeyAuthorizer struct {
storageAccountName string
storageAccountKey string
}

// NewSharedKeyAuthorizer creates a SharedKeyAuthorizer using the given credentials
func NewSharedKeyAuthorizer(accountName, accountKey string) *SharedKeyAuthorizer {
return &SharedKeyAuthorizer{
storageAccountName: accountName,
storageAccountKey: accountKey,
}
}

// WithAuthorization returns a PrepareDecorator that adds an HTTP Authorization header whose
// value is "SharedKey " followed by the computed key.
// This can be used for the Blob, Queue, and File Services
//
// from: https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key
// You may use Shared Key Lite authorization to authorize a request made against the
// 2009-09-19 version and later of the Blob and Queue services,
// and version 2014-02-14 and later of the File services.
func (skl *SharedKeyAuthorizer) WithAuthorization() autorest.PrepareDecorator {
return func(p autorest.Preparer) autorest.Preparer {
return autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) {
r, err := p.Prepare(r)
if err != nil {
return r, err
}

key, err := buildSharedKey(skl.storageAccountName, skl.storageAccountKey, r)
if err != nil {
return r, err
}

sharedKeyHeader := formatSharedKeyAuthorizationHeader(skl.storageAccountName, *key)
return autorest.Prepare(r, autorest.WithHeader(HeaderAuthorization, sharedKeyHeader))
})
}
}
func buildSharedKey(accountName, storageAccountKey string, r *http.Request) (*string, error) {
// first ensure the relevant headers are configured
prepareHeadersForRequest(r)

sharedKey, err := computeSharedKey(r.Method, r.URL.String(), accountName, r.Header)
if err != nil {
return nil, err
}

// we then need to HMAC that value
hmacdValue := hmacValue(storageAccountKey, *sharedKey)
return &hmacdValue, nil
}

// computeSharedKeyLite computes the Shared Key Lite required for Storage Authentication
// NOTE: this function assumes that the `x-ms-date` field is set
func computeSharedKey(verb, url string, accountName string, headers http.Header) (*string, error) {
canonicalizedResource, err := buildCanonicalizedResource(url, accountName, false)
if err != nil {
return nil, err
}

canonicalizedHeaders := buildCanonicalizedHeader(headers)
canonicalizedString := buildCanonicalizedStringForSharedKey(verb, headers, canonicalizedHeaders, *canonicalizedResource)
return &canonicalizedString, nil
}

func buildCanonicalizedStringForSharedKey(verb string, headers http.Header, canonicalizedHeaders, canonicalizedResource string) string {
lengthString := headers.Get(HeaderContentLength)

// empty string when zero
if lengthString == "0" {
lengthString = ""
}

return strings.Join([]string{
verb, // HTTP Verb
headers.Get(HeaderContentEncoding), // Content-Encoding
headers.Get(HeaderContentLanguage), // Content-Language
lengthString, // Content-Length (empty string when zero)
headers.Get(HeaderContentMD5), // Content-MD5
headers.Get(HeaderContentType), // Content-Type
"", // date should be nil, apparently :shrug:
headers.Get(HeaderIfModifiedSince), // If-Modified-Since
headers.Get(HeaderIfMatch), // If-Match
headers.Get(HeaderIfNoneMatch), // If-None-Match
headers.Get(HeaderIfUnmodifiedSince), // If-Unmodified-Since
headers.Get(HeaderRange), // Range
canonicalizedHeaders,
canonicalizedResource,
}, "\n")
}

func formatSharedKeyAuthorizationHeader(accountName, key string) string {
canonicalizedAccountName := primaryStorageAccountName(accountName)
return fmt.Sprintf("SharedKey %s:%s", canonicalizedAccountName, key)
}
6 changes: 3 additions & 3 deletions azurerm/internal/authorizers/authorizer_shared_key_lite.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type SharedKeyLiteAuthorizer struct {
storageAccountKey string
}

// NewSharedKeyLiteAuthorizer crates a SharedKeyLiteAuthorizer using the given credentials
// NewSharedKeyLiteAuthorizer creates a SharedKeyLiteAuthorizer using the given credentials
func NewSharedKeyLiteAuthorizer(accountName, accountKey string) *SharedKeyLiteAuthorizer {
return &SharedKeyLiteAuthorizer{
storageAccountName: accountName,
Expand Down Expand Up @@ -68,7 +68,7 @@ func buildSharedKeyLite(accountName, storageAccountKey string, r *http.Request)
// computeSharedKeyLite computes the Shared Key Lite required for Storage Authentication
// NOTE: this function assumes that the `x-ms-date` field is set
func computeSharedKeyLite(verb, url string, accountName string, headers http.Header) (*string, error) {
canonicalizedResource, err := buildCanonicalizedResource(url, accountName)
canonicalizedResource, err := buildCanonicalizedResource(url, accountName, true)
if err != nil {
return nil, err
}
Expand All @@ -81,7 +81,7 @@ func computeSharedKeyLite(verb, url string, accountName string, headers http.Hea
func buildCanonicalizedStringForSharedKeyLite(verb string, headers http.Header, canonicalizedHeaders, canonicalizedResource string) string {
return strings.Join([]string{
verb,
headers.Get(HeaderContentMD5), // TODO: this appears to always be empty?
headers.Get(HeaderContentMD5),
headers.Get(HeaderContentType),
"", // date should be nil, apparently :shrug:
canonicalizedHeaders,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func buildSharedKeyLiteTable(accountName, storageAccountKey string, r *http.Requ
// NOTE: this function assumes that the `x-ms-date` field is set
func computeSharedKeyLiteTable(url string, accountName string, headers http.Header) (*string, error) {
dateHeader := headers.Get("x-ms-date")
canonicalizedResource, err := buildCanonicalizedResource(url, accountName)
canonicalizedResource, err := buildCanonicalizedResource(url, accountName, true)
if err != nil {
return nil, err
}
Expand Down
33 changes: 29 additions & 4 deletions azurerm/internal/authorizers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func buildCanonicalizedHeader(headers http.Header) string {
}

// buildCanonicalizedResource builds the Canonical Resource required for to sign Storage Account requests
func buildCanonicalizedResource(uri, accountName string) (*string, error) {
func buildCanonicalizedResource(uri, accountName string, sharedKeyLite bool) (*string, error) {
u, err := url.Parse(uri)
if err != nil {
return nil, err
Expand All @@ -67,9 +67,34 @@ func buildCanonicalizedResource(uri, accountName string) (*string, error) {
cr.WriteString(u.EscapedPath())
}

// TODO: replace this with less of a hack
if comp := u.Query().Get("comp"); comp != "" {
cr.WriteString(fmt.Sprintf("?comp=%s", comp))
if sharedKeyLite {
if comp := u.Query().Get("comp"); comp != "" {
cr.WriteString(fmt.Sprintf("?comp=%s", comp))
}
} else {
// Convert all parameter names to lowercase.
// URL-decode each query parameter name and value.
// Sort the query parameters lexicographically by parameter name, in ascending order.
keys := make([]string, 0)
keysWithValues := make(map[string]string, len(u.Query()))
for k, unsortedValues := range u.Query() {
key := strings.ToLower(k)
keys = append(keys, key)

// If a query parameter has more than one value, sort all values lexicographically, then include them in a comma-separated list:
sortedValues := make([]string, 0)
sortedValues = append(sortedValues, unsortedValues...)
sort.Strings(sortedValues)
keysWithValues[key] = strings.Join(sortedValues, ",")
}
sort.Strings(keys)

for _, key := range keys {
values := keysWithValues[key]
// Include a new-line character (\n) before each name-value pair.
// Append each query parameter name and value to the string in the following format, making sure to include the colon (:) between the name and the value
cr.WriteString(fmt.Sprintf("\n%s:%s", key, values))
}
}

out := cr.String()
Expand Down
93 changes: 62 additions & 31 deletions azurerm/internal/authorizers/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,54 +77,85 @@ func TestBuildCanonicalizedHeader(t *testing.T) {

func TestBuildCanonicalizedResource(t *testing.T) {
testData := []struct {
name string
accountName string
uri string
expected string
expectError bool
name string
accountName string
uri string
sharedKeyLite bool
expected string
expectError bool
}{
{
name: "invalid uri",
accountName: "example",
uri: "://example.com",
expected: "",
expectError: true,
name: "invalid uri",
accountName: "example",
uri: "://example.com",
sharedKeyLite: true,
expected: "",
expectError: true,
},
{
name: "storage emulator doesn't get prefix",
accountName: StorageEmulatorAccountName,
uri: "http://www.example.com/foo",
sharedKeyLite: true,
expected: "/foo",
},
{
name: "non storage emulator gets prefix",
accountName: StorageEmulatorAccountName + "test",
uri: "http://www.example.com/foo",
sharedKeyLite: true,
expected: "/" + StorageEmulatorAccountName + "test/foo",
},
{
name: "uri encoding",
accountName: "example",
uri: "<hello>",
sharedKeyLite: true,
expected: "/example%3Chello%3E",
},
{
name: "storage emulator doesn't get prefix",
accountName: StorageEmulatorAccountName,
uri: "http://www.example.com/foo",
expected: "/foo",
name: "comp-arg",
accountName: "example",
uri: "/endpoint?first=true&comp=bar&second=false&third=panda",
sharedKeyLite: true,
expected: "/example/endpoint?comp=bar",
},
{
name: "non storage emulator gets prefix",
accountName: StorageEmulatorAccountName + "test",
uri: "http://www.example.com/foo",
expected: "/" + StorageEmulatorAccountName + "test/foo",
name: "arguments",
accountName: "example",
uri: "/endpoint?first=true&second=false&third=panda",
sharedKeyLite: true,
expected: "/example/endpoint",
},
{
name: "uri encoding",
accountName: "example",
uri: "<hello>",
expected: "/example%3Chello%3E",
name: "arguments-sharedkey",
accountName: "example",
uri: "/endpoint?first=true&second=false&third=panda",
sharedKeyLite: false,
expected: "/example/endpoint\nfirst:true\nsecond:false\nthird:panda",
expectError: false,
},
{
name: "comp-arg",
accountName: "example",
uri: "/endpoint?first=true&comp=bar&second=false&third=panda",
expected: "/example/endpoint?comp=bar",
name: "arguments-sharedkey",
accountName: "example",
uri: "/endpoint?comp=strawberries&restype=pandas",
sharedKeyLite: false,
expected: "/example/endpoint\ncomp:strawberries\nrestype:pandas",
expectError: false,
},
{
name: "arguments",
accountName: "example",
uri: "/endpoint?first=true&second=false&third=panda",
expected: "/example/endpoint",
name: "extra-arguments-sharedkey",
accountName: "myaccount",
uri: "/mycontainer?restype=container&comp=list&include=snapshots&include=metadata&include=uncommittedblobs",
expected: "/myaccount/mycontainer\ncomp:list\ninclude:metadata,snapshots,uncommittedblobs\nrestype:container",
sharedKeyLite: false,
expectError: false,
},
}

for _, test := range testData {
t.Logf("Test %q", test.name)
actual, err := buildCanonicalizedResource(test.uri, test.accountName)
actual, err := buildCanonicalizedResource(test.uri, test.accountName, test.sharedKeyLite)
if err != nil {
if test.expectError {
continue
Expand Down
4 changes: 2 additions & 2 deletions azurerm/internal/services/storage/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (client Client) BlobsClient(ctx context.Context, resourceGroup, accountName
return nil, fmt.Errorf("Error retrieving Account Key: %s", err)
}

storageAuth := authorizers.NewSharedKeyLiteAuthorizer(accountName, *accountKey)
storageAuth := authorizers.NewSharedKeyAuthorizer(accountName, *accountKey)
blobsClient := blobs.NewWithEnvironment(client.environment)
blobsClient.Client.Authorizer = storageAuth
return &blobsClient, nil
Expand All @@ -53,7 +53,7 @@ func (client Client) ContainersClient(ctx context.Context, resourceGroup, accoun
return nil, fmt.Errorf("Error retrieving Account Key: %s", err)
}

storageAuth := authorizers.NewSharedKeyLiteAuthorizer(accountName, *accountKey)
storageAuth := authorizers.NewSharedKeyAuthorizer(accountName, *accountKey)
containersClient := containers.NewWithEnvironment(client.environment)
containersClient.Client.Authorizer = storageAuth
return &containersClient, nil
Expand Down
Loading