-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Storage: adding an Authorizer for Storage via SharedKeyLite
- Loading branch information
1 parent
3fb628c
commit 4c37673
Showing
5 changed files
with
536 additions
and
0 deletions.
There are no files selected for viewing
90 changes: 90 additions & 0 deletions
90
azurerm/internal/authorizers/authorizer_shared_key_lite.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package authorizers | ||
|
||
import ( | ||
"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 | ||
|
||
// SharedKeyLiteAuthorizer implements an authorization for Shared Key Lite | ||
// this can be used for interaction with Blob, File and Queue Storage Endpoints | ||
type SharedKeyLiteAuthorizer struct { | ||
storageAccountName string | ||
storageAccountKey string | ||
} | ||
|
||
// NewSharedKeyLiteAuthorizer crates a SharedKeyLiteAuthorizer using the given credentials | ||
func NewSharedKeyLiteAuthorizer(accountName, accountKey string) *SharedKeyLiteAuthorizer { | ||
return &SharedKeyLiteAuthorizer{ | ||
storageAccountName: accountName, | ||
storageAccountKey: accountKey, | ||
} | ||
} | ||
|
||
// WithAuthorization returns a PrepareDecorator that adds an HTTP Authorization header whose | ||
// value is "SharedKeyLite " 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 *SharedKeyLiteAuthorizer) 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 := buildSharedKeyLite(skl.storageAccountName, skl.storageAccountKey, r) | ||
if err != nil { | ||
return r, err | ||
} | ||
|
||
sharedKeyHeader := formatSharedKeyLiteAuthorizationHeader(skl.storageAccountName, *key) | ||
return autorest.Prepare(r, autorest.WithHeader(HeaderAuthorization, sharedKeyHeader)) | ||
}) | ||
} | ||
} | ||
func buildSharedKeyLite(accountName, storageAccountKey string, r *http.Request) (*string, error) { | ||
// first ensure the relevant headers are configured | ||
prepareHeadersForRequest(r) | ||
|
||
sharedKey, err := computeSharedKeyLite(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 computeSharedKeyLite(verb, url string, accountName string, headers http.Header) (*string, error) { | ||
canonicalizedResource, err := buildCanonicalizedResource(url, accountName) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
canonicalizedHeaders := buildCanonicalizedHeader(headers) | ||
canonicalizedString := buildCanonicalizedStringForSharedKeyLite(verb, headers, canonicalizedHeaders, *canonicalizedResource) | ||
return &canonicalizedString, nil | ||
} | ||
|
||
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(HeaderContentType), | ||
"", // date should be nil, apparently :shrug: | ||
canonicalizedHeaders, | ||
canonicalizedResource, | ||
}, "\n") | ||
} |
36 changes: 36 additions & 0 deletions
36
azurerm/internal/authorizers/authorizer_shared_key_lite_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package authorizers | ||
|
||
import ( | ||
"testing" | ||
) | ||
|
||
func TestBuildCanonicalizedStringForSharedKeyLite(t *testing.T) { | ||
testData := []struct { | ||
name string | ||
headers map[string][]string | ||
canonicalizedHeaders string | ||
canonicalizedResource string | ||
verb string | ||
expected string | ||
}{ | ||
{ | ||
name: "completed", | ||
verb: "NOM", | ||
headers: map[string][]string{ | ||
"Content-MD5": {"abc123"}, | ||
"Content-Type": {"vnd/panda-pops+v1"}, | ||
}, | ||
canonicalizedHeaders: "all-the-headers", | ||
canonicalizedResource: "all-the-resources", | ||
expected: "NOM\n\nvnd/panda-pops+v1\n\nall-the-headers\nall-the-resources", | ||
}, | ||
} | ||
|
||
for _, test := range testData { | ||
t.Logf("Test: %q", test.name) | ||
actual := buildCanonicalizedStringForSharedKeyLite(test.verb, test.headers, test.canonicalizedHeaders, test.canonicalizedResource) | ||
if actual != test.expected { | ||
t.Fatalf("Expected %q but got %q", test.expected, actual) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package authorizers | ||
|
||
var ( | ||
HeaderAuthorization = "Authorization" | ||
HeaderContentLength = "Content-Length" | ||
HeaderContentEncoding = "Content-Encoding" | ||
HeaderContentLanguage = "Content-Language" | ||
HeaderContentType = "Content-Type" | ||
HeaderContentMD5 = "Content-MD5" | ||
HeaderIfModifiedSince = "If-Modified-Since" | ||
HeaderIfMatch = "If-Match" | ||
HeaderIfNoneMatch = "If-None-Match" | ||
HeaderIfUnmodifiedSince = "If-Unmodified-Since" | ||
HeaderMSDate = "X-Ms-Date" | ||
HeaderRange = "Range" | ||
|
||
StorageEmulatorAccountName = "devstoreaccount1" | ||
StorageEmulatorAccountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
package authorizers | ||
|
||
import ( | ||
"bytes" | ||
"crypto/hmac" | ||
"crypto/sha256" | ||
"encoding/base64" | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
"sort" | ||
"strings" | ||
"time" | ||
) | ||
|
||
// buildCanonicalizedHeader builds the Canonicalized Header required to sign Storage Requests | ||
func buildCanonicalizedHeader(headers http.Header) string { | ||
cm := make(map[string]string) | ||
|
||
for k, v := range headers { | ||
headerName := strings.TrimSpace(strings.ToLower(k)) | ||
if strings.HasPrefix(headerName, "x-ms-") { | ||
cm[headerName] = v[0] | ||
} | ||
} | ||
|
||
if len(cm) == 0 { | ||
return "" | ||
} | ||
|
||
var keys []string | ||
for key := range cm { | ||
keys = append(keys, key) | ||
} | ||
|
||
sort.Strings(keys) | ||
|
||
ch := bytes.NewBufferString("") | ||
|
||
for _, key := range keys { | ||
ch.WriteString(key) | ||
ch.WriteRune(':') | ||
ch.WriteString(cm[key]) | ||
ch.WriteRune('\n') | ||
} | ||
|
||
return strings.TrimSuffix(string(ch.Bytes()), "\n") | ||
} | ||
|
||
// buildCanonicalizedResource builds the Canonical Resource required for to sign Storage Account requests | ||
func buildCanonicalizedResource(uri, accountName string) (*string, error) { | ||
u, err := url.Parse(uri) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
cr := bytes.NewBufferString("") | ||
if accountName != StorageEmulatorAccountName { | ||
cr.WriteString("/") | ||
cr.WriteString(primaryStorageAccountName(accountName)) | ||
} | ||
|
||
if len(u.Path) > 0 { | ||
// Any portion of the CanonicalizedResource string that is derived from | ||
// the resource's URI should be encoded exactly as it is in the URI. | ||
// -- https://msdn.microsoft.com/en-gb/library/azure/dd179428.aspx | ||
cr.WriteString(u.EscapedPath()) | ||
} | ||
|
||
// TODO: replace this with less of a hack | ||
if comp := u.Query().Get("comp"); comp != "" { | ||
cr.WriteString(fmt.Sprintf("?comp=%s", comp)) | ||
} | ||
|
||
out := string(cr.Bytes()) | ||
return &out, nil | ||
} | ||
|
||
func formatSharedKeyLiteAuthorizationHeader(accountName, key string) string { | ||
canonicalizedAccountName := primaryStorageAccountName(accountName) | ||
return fmt.Sprintf("SharedKeyLite %s:%s", canonicalizedAccountName, key) | ||
} | ||
|
||
// hmacValue base-64 decodes the storageAccountKey, then signs the string with it | ||
// as outlined here: https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key | ||
func hmacValue(storageAccountKey, canonicalizedString string) string { | ||
key, err := base64.StdEncoding.DecodeString(storageAccountKey) | ||
if err != nil { | ||
return "" | ||
} | ||
|
||
encr := hmac.New(sha256.New, []byte(key)) | ||
encr.Write([]byte(canonicalizedString)) | ||
return base64.StdEncoding.EncodeToString(encr.Sum(nil)) | ||
} | ||
|
||
// prepareHeadersForRequest prepares a request so that it can be signed | ||
// by ensuring the `date` and `x-ms-date` headers are set | ||
func prepareHeadersForRequest(r *http.Request) { | ||
if r.Header == nil { | ||
r.Header = http.Header{} | ||
} | ||
|
||
date := time.Now().UTC().Format(http.TimeFormat) | ||
|
||
// a date must be set, X-Ms-Date should be used when both are set; but let's set both for completeness | ||
r.Header.Set("date", date) | ||
r.Header.Set("x-ms-date", date) | ||
} | ||
|
||
// primaryStorageAccountName returns the name of the primary for a given Storage Account | ||
func primaryStorageAccountName(input string) string { | ||
// from https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key | ||
// If you are accessing the secondary location in a storage account for which | ||
// read-access geo-replication (RA-GRS) is enabled, do not include the | ||
// -secondary designation in the authorization header. | ||
// For authorization purposes, the account name is always the name of the primary location, | ||
// even for secondary access. | ||
return strings.TrimSuffix(input, "-secondary") | ||
} |
Oops, something went wrong.