diff --git a/storage/blobsasuri.go b/storage/blobsasuri.go index 81f9fb8d4c75..09042306e95e 100644 --- a/storage/blobsasuri.go +++ b/storage/blobsasuri.go @@ -28,17 +28,6 @@ type BlobSASOptions struct { SASOptions } -// SASOptions includes options used by SAS URIs for different -// services and resources. -type SASOptions struct { - APIVersion string - Start time.Time - Expiry time.Time - IP string - UseHTTPS bool - Identifier string -} - // BlobServiceSASPermissions includes the available permissions for // blob service SAS URI. type BlobServiceSASPermissions struct { @@ -141,13 +130,6 @@ func (c *Client) blobAndFileSASURI(options SASOptions, uri, permissions, canonic return sasURL.String(), nil } -func addQueryParameter(query url.Values, key, value string) url.Values { - if value != "" { - query.Add(key, value) - } - return query -} - func blobSASStringToSign(signedPermissions, signedStart, signedExpiry, canonicalizedResource, signedIdentifier, signedIP, protocols, signedVersion string, headers OverrideHeaders) (string, error) { rscc := headers.CacheControl rscd := headers.ContentDisposition diff --git a/storage/commonsasuri.go b/storage/commonsasuri.go new file mode 100644 index 000000000000..674dc195ff84 --- /dev/null +++ b/storage/commonsasuri.go @@ -0,0 +1,24 @@ +package storage + +import ( + "net/url" + "time" +) + +// SASOptions includes options used by SAS URIs for different +// services and resources. +type SASOptions struct { + APIVersion string + Start time.Time + Expiry time.Time + IP string + UseHTTPS bool + Identifier string +} + +func addQueryParameter(query url.Values, key, value string) url.Values { + if value != "" { + query.Add(key, value) + } + return query +} diff --git a/storage/queuesasuri.go b/storage/queuesasuri.go new file mode 100644 index 000000000000..406db9e36191 --- /dev/null +++ b/storage/queuesasuri.go @@ -0,0 +1,132 @@ +package storage + +import ( + "errors" + "fmt" + "net/url" + "strings" + "time" +) + +// QueueSASOptions are options to construct a blob SAS +// URI. +// See https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-a-service-sas +type QueueSASOptions struct { + QueueSASPermissions + SASOptions +} + +// QueueSASPermissions includes the available permissions for +// a queue SAS URI. +type QueueSASPermissions struct { + Read bool + Add bool + Update bool + Process bool +} + +func (q QueueSASPermissions) buildString() string { + permissions := "" + + if q.Read { + permissions += "r" + } + if q.Add { + permissions += "a" + } + if q.Update { + permissions += "u" + } + if q.Process { + permissions += "p" + } + return permissions +} + +// GetSASURI creates an URL to the specified queue which contains the Shared +// Access Signature with specified permissions and expiration time. +// +// See https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-a-service-sas +func (q *Queue) GetSASURI(options QueueSASOptions) (string, error) { + canonicalizedResource, err := q.qsc.client.buildCanonicalizedResource(q.buildPath(), q.qsc.auth) + if err != nil { + return "", err + } + + // "The canonicalizedresouce portion of the string is a canonical path to the signed resource. + // It must include the service name (blob, table, queue or file) for version 2015-02-21 or + // later, the storage account name, and the resource name, and must be URL-decoded. + // -- https://msdn.microsoft.com/en-us/library/azure/dn140255.aspx + // We need to replace + with %2b first to avoid being treated as a space (which is correct for query strings, but not the path component). + canonicalizedResource = strings.Replace(canonicalizedResource, "+", "%2b", -1) + canonicalizedResource, err = url.QueryUnescape(canonicalizedResource) + if err != nil { + return "", err + } + + signedStart := "" + if options.Start != (time.Time{}) { + signedStart = options.Start.UTC().Format(time.RFC3339) + } + signedExpiry := options.Expiry.UTC().Format(time.RFC3339) + + protocols := "https,http" + if options.UseHTTPS { + protocols = "https" + } + + permissions := options.QueueSASPermissions.buildString() + stringToSign, err := queueSASStringToSign(q.qsc.client.apiVersion, canonicalizedResource, signedStart, signedExpiry, options.IP, permissions, protocols, options.Identifier) + if err != nil { + return "", err + } + + sig := q.qsc.client.computeHmac256(stringToSign) + sasParams := url.Values{ + "sv": {q.qsc.client.apiVersion}, + "se": {signedExpiry}, + "sp": {permissions}, + "sig": {sig}, + } + + if q.qsc.client.apiVersion >= "2015-04-05" { + sasParams.Add("spr", protocols) + addQueryParameter(sasParams, "sip", options.IP) + } + + uri := q.qsc.client.getEndpoint(queueServiceName, q.buildPath(), nil) + sasURL, err := url.Parse(uri) + if err != nil { + return "", err + } + sasURL.RawQuery = sasParams.Encode() + return sasURL.String(), nil +} + +func queueSASStringToSign(signedVersion, canonicalizedResource, signedStart, signedExpiry, signedIP, signedPermissions, protocols, signedIdentifier string) (string, error) { + + if signedVersion >= "2015-02-21" { + canonicalizedResource = "/queue" + canonicalizedResource + } + + // https://msdn.microsoft.com/en-us/library/azure/dn140255.aspx#Anchor_12 + if signedVersion >= "2015-04-05" { + return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s", + signedPermissions, + signedStart, + signedExpiry, + canonicalizedResource, + signedIdentifier, + signedIP, + protocols, + signedVersion), nil + + } + + // reference: http://msdn.microsoft.com/en-us/library/azure/dn140255.aspx + if signedVersion >= "2013-08-15" { + return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", signedPermissions, signedStart, signedExpiry, canonicalizedResource, signedIdentifier, signedVersion), nil + } + + return "", errors.New("storage: not implemented SAS for versions earlier than 2013-08-15") +} diff --git a/storage/queuesasuri_test.go b/storage/queuesasuri_test.go new file mode 100644 index 000000000000..6c84006c392e --- /dev/null +++ b/storage/queuesasuri_test.go @@ -0,0 +1,107 @@ +package storage + +import ( + "net/url" + "time" + + chk "gopkg.in/check.v1" +) + +type QueueSASURISuite struct{} + +var _ = chk.Suite(&QueueSASURISuite{}) + +var queueOldAPIVer = "2013-08-15" +var queueNewerAPIVer = "2015-04-05" + +func (s *QueueSASURISuite) TestGetQueueSASURI(c *chk.C) { + api, err := NewClient("foo", dummyMiniStorageKey, DefaultBaseURL, queueOldAPIVer, true) + c.Assert(err, chk.IsNil) + cli := api.GetQueueService() + q := cli.GetQueueReference("name") + + expectedParts := url.URL{ + Scheme: "https", + Host: "foo.queue.core.windows.net", + Path: "name", + RawQuery: url.Values{ + "sv": {oldAPIVer}, + "sig": {"dYZ+elcEz3ZXEnTDKR5+RCrMzk0L7/ATWsemNzb36VM="}, + "sp": {"p"}, + "se": {"0001-01-01T00:00:00Z"}, + }.Encode()} + + options := QueueSASOptions{} + options.Process = true + options.Expiry = time.Time{} + + u, err := q.GetSASURI(options) + c.Assert(err, chk.IsNil) + sasParts, err := url.Parse(u) + c.Assert(err, chk.IsNil) + c.Assert(expectedParts.String(), chk.Equals, sasParts.String()) + c.Assert(expectedParts.Query(), chk.DeepEquals, sasParts.Query()) +} + +func (s *QueueSASURISuite) TestGetQueueSASURIWithSignedIPValidAPIVersionPassed(c *chk.C) { + api, err := NewClient("foo", dummyMiniStorageKey, DefaultBaseURL, queueNewerAPIVer, true) + c.Assert(err, chk.IsNil) + cli := api.GetQueueService() + q := cli.GetQueueReference("name") + + expectedParts := url.URL{ + Scheme: "https", + Host: "foo.queue.core.windows.net", + Path: "/name", + RawQuery: url.Values{ + "sv": {newerAPIVer}, + "sig": {"8uvfE93HdYxQ3xvt/CUN3S7sYEl1LcuHBC0oYoGDnfw="}, + "sip": {"127.0.0.1"}, + "sp": {"p"}, + "se": {"0001-01-01T00:00:00Z"}, + "spr": {"https,http"}, + }.Encode()} + + options := QueueSASOptions{} + options.Process = true + options.Expiry = time.Time{} + options.IP = "127.0.0.1" + + u, err := q.GetSASURI(options) + c.Assert(err, chk.IsNil) + sasParts, err := url.Parse(u) + c.Assert(err, chk.IsNil) + c.Assert(sasParts.Query(), chk.DeepEquals, expectedParts.Query()) +} + +// Trying to use SignedIP but using an older version of the API. +// Should ignore the signedIP and just use what the older version requires. +func (s *QueueSASURISuite) TestGetQueueSASURIWithSignedIPUsingOldAPIVersion(c *chk.C) { + api, err := NewClient("foo", dummyMiniStorageKey, DefaultBaseURL, oldAPIVer, true) + c.Assert(err, chk.IsNil) + cli := api.GetQueueService() + q := cli.GetQueueReference("name") + + expectedParts := url.URL{ + Scheme: "https", + Host: "foo.queue.core.windows.net", + Path: "/name", + RawQuery: url.Values{ + "sv": {oldAPIVer}, + "sig": {"dYZ+elcEz3ZXEnTDKR5+RCrMzk0L7/ATWsemNzb36VM="}, + "sp": {"p"}, + "se": {"0001-01-01T00:00:00Z"}, + }.Encode()} + + options := QueueSASOptions{} + options.Process = true + options.Expiry = time.Time{} + options.IP = "127.0.0.1" + + u, err := q.GetSASURI(options) + c.Assert(err, chk.IsNil) + sasParts, err := url.Parse(u) + c.Assert(err, chk.IsNil) + c.Assert(expectedParts.String(), chk.Equals, sasParts.String()) + c.Assert(expectedParts.Query(), chk.DeepEquals, sasParts.Query()) +}