Skip to content

Commit

Permalink
Storage: adding an Authorizer for Storage via SharedKeyLite
Browse files Browse the repository at this point in the history
  • Loading branch information
tombuildsstuff committed Jul 7, 2019
1 parent 3fb628c commit 4c37673
Show file tree
Hide file tree
Showing 5 changed files with 536 additions and 0 deletions.
90 changes: 90 additions & 0 deletions azurerm/internal/authorizers/authorizer_shared_key_lite.go
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 azurerm/internal/authorizers/authorizer_shared_key_lite_test.go
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)
}
}
}
19 changes: 19 additions & 0 deletions azurerm/internal/authorizers/consts.go
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=="
)
120 changes: 120 additions & 0 deletions azurerm/internal/authorizers/helpers.go
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")
}
Loading

0 comments on commit 4c37673

Please sign in to comment.