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

Queuesas #657

Merged
merged 14 commits into from
Jul 3, 2017
18 changes: 0 additions & 18 deletions storage/blobsasuri.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions storage/commonsasuri.go
Original file line number Diff line number Diff line change
@@ -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
}
132 changes: 132 additions & 0 deletions storage/queuesasuri.go
Original file line number Diff line number Diff line change
@@ -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")
}
107 changes: 107 additions & 0 deletions storage/queuesasuri_test.go
Original file line number Diff line number Diff line change
@@ -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())
}