Merge lp:~rvb/gwacl/signed-access into lp:gwacl

Proposed by Raphaël Badin
Status: Merged
Approved by: Raphaël Badin
Approved revision: 206
Merged at revision: 202
Proposed branch: lp:~rvb/gwacl/signed-access
Merge into: lp:gwacl
Diff against target: 256 lines (+201/-3)
4 files modified
shared_signature.go (+74/-0)
shared_signature_test.go (+70/-0)
storage_base.go (+19/-3)
storage_base_test.go (+38/-0)
To merge this branch: bzr merge lp:~rvb/gwacl/signed-access
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+176732@code.launchpad.net

Commit message

Add support for the Windows Azure Shared Access Signature URL mechanism.

Description of the change

This branch adds support for the Windows Azure Shared Access Signature URL mechanism. More specifically, it adds a way to get an anonymously-accessible url for a blob (even if the blob is located in a private storage).

See http://msdn.microsoft.com/en-us/library/windowsazure/dn140255.aspx for details.

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) wrote :
Download full text (5.0 KiB)

Nice branch, reasonably well tested, but I think we should improve some of that testing!

In particular, the new sign() method is not unit tested. I think you should add a test for it, and then we can use that function in other tests like TestComposeSharedSignature so that we don't have to hard-code the magic string "COJt8UagHJR2LBT1129bhDChtgfLGFqfZ0YQpBdF0=" which makes the test difficult to verify what it's actually testing.

Ok so on to the details:

16 +func composeSharedSignature(permission, signedExpiry, path, accountName, signedIdentifier, signedVersion, accountKey string) string {

Since you documented the other internal compose* funcs, can you do the same here please.

19 + stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
20 + permission, "", signedExpiry, canonicalizedResource, signedIdentifier, signedVersion)

Gah I *really* miss Python's format() here. The second arg of "" is a bit magic, can you initalise a variable called 'signedStart' to "" and then use it instead of the bare "", it will make that Sprintf more readable (the "" sticks out like a pimple on a supermodel).

29 +func composeAccessQueryString(permission, signedExpiry, path, accountName, signedIdentifier, signedVersion, signedRessource, accountKey string) url.Values {

Given that this shares almost all of its params with composeSharedSignature it might be worth declaring a struct type which encapsulates the data in a shared access signature. I think once functions get beyond more than three params they get very unwieldy very quickly. The new type could well be useful in the future if we do more things with SASes.

Also this function is mis-named, it is not composing a string. May I suggest composeAcessQueryValues instead.

+func composeReadBlobValues(container, filename, accountName, accountKey string, expires time.Time) url.Values {
45 + expiryDateString := expires.UTC().Format(time.RFC3339)

Christ - why did they not make the date header format and this expiry stamp consistent? :/ (The other is RFC1123)

71 +func (*sharedSignatureSuite) TestComposeSharedSignature(c *C) {
72 + permission := "r"
73 + signedExpiry := "2015-02-12"
74 + path := "/path/to/file"
75 + accountName := "name"
76 + signedIdentifier := "identifier"
77 + signedVersion := "2012-02-12"
78 + accountKey := base64.StdEncoding.EncodeToString([]byte("key"))
79 +
80 + signature := composeSharedSignature(permission, signedExpiry, path, accountName, signedIdentifier, signedVersion, accountKey)
81 + c.Check(signature, Equals, "C/COJt8UagHJR2LBT1129bhDChtgfLGFqfZ0YQpBdF0=")

Here, once sign() is independently tested, you can use its return value instead of hard-coding this string here. The test will improve in readability for that.

103 +func (*sharedSignatureSuite) TestComposeReadBlobValues(c *C) {
104 + container := "container"
105 + filename := "/path/to/file"
106 + accountName := "name"
107 + accountKey := base64.StdEncoding.EncodeToString([]byte("key"))
108 + expires := time.Now()
109 + values := composeReadBlobValues(container, filename, accountName, accountKey, expires)
110 + c.Check(values.Get("sv"), Equals, "2012-02-12")
111 + ...

Read more...

review: Needs Fixing
Revision history for this message
Julian Edwards (julian-edwards) wrote :

Early approval, I am sure you'll fix things :)

review: Approve
lp:~rvb/gwacl/signed-access updated
206. By Raphaël Badin

Review fixes.

Revision history for this message
Raphaël Badin (rvb) wrote :
Download full text (6.4 KiB)

> Nice branch, reasonably well tested, but I think we should improve some of
> that testing!

Thanks for the review!

> In particular, the new sign() method is not unit tested. I think you should
> add a test for it, and then we can use that function in other tests like
> TestComposeSharedSignature so that we don't have to hard-code the magic string
> "COJt8UagHJR2LBT1129bhDChtgfLGFqfZ0YQpBdF0=" which makes the test difficult to
> verify what it's actually testing.

Like I said on IRC, this is not a new method, it's just extracted from somewhere but it was already there (and tested by a method which calls it)… but you're right, having a unit-test of the method itself is much better; done.

> 16 +func composeSharedSignature(permission, signedExpiry, path,
> accountName, signedIdentifier, signedVersion, accountKey string) string {
>
> Since you documented the other internal compose* funcs, can you do the same
> here please.

In this case, I felt the name of the method would be as good as any comment but okay, done.

> 19 + stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
> 20 + permission, "", signedExpiry, canonicalizedResource,
> signedIdentifier, signedVersion)
>
> Gah I *really* miss Python's format() here. The second arg of "" is a bit
> magic, can you initalise a variable called 'signedStart' to "" and then use it
> instead of the bare "", it will make that Sprintf more readable (the "" sticks
> out like a pimple on a supermodel).

Very good point, done (I've added a 'signedStart' field to the structure).

> 29 +func composeAccessQueryString(permission, signedExpiry, path,
> accountName, signedIdentifier, signedVersion, signedRessource, accountKey
> string) url.Values {
>
> Given that this shares almost all of its params with composeSharedSignature it
> might be worth declaring a struct type which encapsulates the data in a shared
> access signature. I think once functions get beyond more than three params
> they get very unwieldy very quickly. The new type could well be useful in the
> future if we do more things with SASes.

Excellent point, done.

> Also this function is mis-named, it is not composing a string. May I suggest
> composeAcessQueryValues instead.

True, I started with a method which returned a string then changed the signature but not the name, thanks for spotting that!

> 71 +func (*sharedSignatureSuite) TestComposeSharedSignature(c *C) {
> 72 + permission := "r"
> 73 + signedExpiry := "2015-02-12"
> 74 + path := "/path/to/file"
> 75 + accountName := "name"
> 76 + signedIdentifier := "identifier"
> 77 + signedVersion := "2012-02-12"
> 78 + accountKey := base64.StdEncoding.EncodeToString([]byte("key"))
> 79 +
> 80 + signature := composeSharedSignature(permission, signedExpiry,
> path, accountName, signedIdentifier, signedVersion, accountKey)
> 81 + c.Check(signature, Equals,
> "C/COJt8UagHJR2LBT1129bhDChtgfLGFqfZ0YQpBdF0=")
>
> Here, once sign() is independently tested, you can use its return value
> instead of hard-coding this string here. The test will improve in readability
> for that.

That wou...

Read more...

Revision history for this message
Raphaël Badin (rvb) wrote :

> Early approval, I am sure you'll fix things :)

I did, thanks for the review :)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'shared_signature.go'
2--- shared_signature.go 1970-01-01 00:00:00 +0000
3+++ shared_signature.go 2013-07-26 08:51:26 +0000
4@@ -0,0 +1,74 @@
5+// Copyright 2013 Canonical Ltd. This software is licensed under the
6+// GNU Lesser General Public License version 3 (see the file COPYING).
7+
8+package gwacl
9+
10+import (
11+ "fmt"
12+ "net/url"
13+ "time"
14+)
15+
16+// sharedSignatureParams is a object which encapsulate all the parameters
17+// required to delegate access to a Windows Azure object using the
18+// "Shared Access Signature" mechanism.
19+type sharedSignatureParams struct {
20+ permission string
21+ signedStart string
22+ signedExpiry string
23+ path string
24+ accountName string
25+ signedIdentifier string
26+ signedVersion string
27+ signedRessource string
28+ accountKey string
29+}
30+
31+// composeSharedSignature composes the "Shared Access Signature" as described
32+// in the paragraph "Constructing the Signature String" in
33+// http://msdn.microsoft.com/en-us/library/windowsazure/dn140255.aspx
34+func (params *sharedSignatureParams) composeSharedSignature() string {
35+ // Compose the string to sign.
36+ canonicalizedResource := fmt.Sprintf("/%s%s", params.accountName, params.path)
37+ stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
38+ params.permission, params.signedStart, params.signedExpiry, canonicalizedResource, params.signedIdentifier, params.signedVersion)
39+ // Create the signature.
40+ signature := sign(params.accountKey, stringToSign)
41+ return signature
42+}
43+
44+// composeAccessQueryValues returns the values that correspond to the query
45+// string used to build a Shared Access Signature URI as described in
46+// http://msdn.microsoft.com/en-us/library/windowsazure/dn140255.aspx
47+func (params *sharedSignatureParams) composeAccessQueryValues() url.Values {
48+ signature := params.composeSharedSignature()
49+ // Compose the "Shared Access Signature" query string.
50+ values := url.Values{}
51+ values.Set("sv", params.signedVersion)
52+ values.Set("se", params.signedExpiry)
53+ values.Set("sr", params.signedRessource)
54+ values.Set("sp", params.permission)
55+ values.Set("sig", signature)
56+ return values
57+}
58+
59+// getReadBlobAccessValues returns the values that correspond to the query
60+// string used to build a Shared Access Signature URI. The signature grants
61+// read access to the given blob.
62+func getReadBlobAccessValues(container, filename, accountName, accountKey string, expires time.Time) url.Values {
63+ expiryDateString := expires.UTC().Format(time.RFC3339)
64+
65+ path := fmt.Sprintf("/%s/%s", container, filename)
66+ signatureParams := &sharedSignatureParams{
67+ permission: "r",
68+ signedExpiry: expiryDateString,
69+ signedStart: "",
70+ path: path,
71+ accountName: accountName,
72+ signedIdentifier: "",
73+ signedVersion: "2012-02-12",
74+ signedRessource: "b",
75+ accountKey: accountKey,
76+ }
77+ return signatureParams.composeAccessQueryValues()
78+}
79
80=== added file 'shared_signature_test.go'
81--- shared_signature_test.go 1970-01-01 00:00:00 +0000
82+++ shared_signature_test.go 2013-07-26 08:51:26 +0000
83@@ -0,0 +1,70 @@
84+// Copyright 2013 Canonical Ltd. This software is licensed under the
85+// GNU Lesser General Public License version 3 (see the file COPYING).
86+
87+package gwacl
88+
89+import (
90+ "encoding/base64"
91+ . "launchpad.net/gocheck"
92+ "time"
93+)
94+
95+type sharedSignatureSuite struct{}
96+
97+var _ = Suite(&sharedSignatureSuite{})
98+
99+func (*sharedSignatureSuite) TestComposeSharedSignature(c *C) {
100+ params := &sharedSignatureParams{
101+ permission: "r",
102+ signedExpiry: "2015-02-12",
103+ path: "/path/to/file",
104+ accountName: "name",
105+ signedIdentifier: "identifier",
106+ signedVersion: "2012-02-12",
107+ signedRessource: "/the/ressource",
108+ accountKey: base64.StdEncoding.EncodeToString([]byte("key")),
109+ }
110+
111+ signature := params.composeSharedSignature()
112+ c.Check(signature, Equals, "C/COJt8UagHJR2LBT1129bhDChtgfLGFqfZ0YQpBdF0=")
113+}
114+
115+func (*sharedSignatureSuite) TestComposeAccessQueryValues(c *C) {
116+ params := &sharedSignatureParams{
117+ permission: "r",
118+ signedExpiry: "2015-02-12",
119+ path: "/path/to/file",
120+ accountName: "name",
121+ signedIdentifier: "identifier",
122+ signedVersion: "2012-02-12",
123+ signedRessource: "/the/ressource",
124+ accountKey: base64.StdEncoding.EncodeToString([]byte("key")),
125+ }
126+
127+ values := params.composeAccessQueryValues()
128+
129+ c.Check(values.Get("sv"), Equals, params.signedVersion)
130+ c.Check(values.Get("se"), Equals, params.signedExpiry)
131+ c.Check(values.Get("sr"), Equals, params.signedRessource)
132+ c.Check(values.Get("sp"), Equals, params.permission)
133+ c.Check(values.Get("sig"), Not(HasLen), 0)
134+}
135+
136+func (*sharedSignatureSuite) TestGetReadBlobAccessValues(c *C) {
137+ container := "container"
138+ filename := "/path/to/file"
139+ accountName := "name"
140+ accountKey := base64.StdEncoding.EncodeToString([]byte("key"))
141+ expires, err := time.Parse("Monday, 02-Jan-06 15:04:05 MST", time.RFC850)
142+ c.Assert(err, IsNil)
143+
144+ values := getReadBlobAccessValues(container, filename, accountName, accountKey, expires)
145+
146+ c.Check(values.Get("sv"), Equals, "2012-02-12")
147+ expiryDateString := values.Get("se")
148+ expectedExpiryDateString := expires.UTC().Format(time.RFC3339)
149+ c.Check(expiryDateString, Equals, expectedExpiryDateString)
150+ c.Check(values.Get("sr"), Equals, "b")
151+ c.Check(values.Get("sp"), Equals, "r")
152+ c.Check(values.Get("sig"), Equals, "HK7xmxiUY/vBNkaIWoJkIcv27g/+QFjwKVgO/I3yWmI=")
153+}
154
155=== modified file 'storage_base.go'
156--- storage_base.go 2013-06-27 11:53:50 +0000
157+++ storage_base.go 2013-07-26 08:51:26 +0000
158@@ -41,9 +41,9 @@
159 "Range",
160 }
161
162-// Calculate the value required for an Authorization header.
163-func composeAuthHeader(req *http.Request, accountName, accountKey string) string {
164- signable := composeStringToSign(req, accountName)
165+// sign returns the base64-encoded HMAC-SHA256 signature of the given string
166+// using the given base64-encoded key.
167+func sign(accountKey, signable string) string {
168 // Allegedly, this is already UTF8 encoded.
169 decodedKey, err := base64.StdEncoding.DecodeString(accountKey)
170 if err != nil {
171@@ -57,6 +57,14 @@
172 var hashed []byte
173 hashed = hash.Sum(hashed)
174 b64Hashed := base64.StdEncoding.EncodeToString(hashed)
175+ return b64Hashed
176+}
177+
178+// Calculate the value required for an Authorization header.
179+func composeAuthHeader(req *http.Request, accountName, accountKey string) string {
180+ signable := composeStringToSign(req, accountName)
181+
182+ b64Hashed := sign(accountKey, signable)
183 return fmt.Sprintf("SharedKey %s:%s", accountName, b64Hashed)
184 }
185
186@@ -338,6 +346,14 @@
187 return context.getContainerURL(container) + "/" + url.QueryEscape(filename)
188 }
189
190+// GetAnonymousFileURL returns an anonymously-accessible URL for a given file
191+// in the given container.
192+func (context *StorageContext) GetAnonymousFileURL(container, filename string, expires time.Time) string {
193+ url := context.GetFileURL(container, filename)
194+ values := getReadBlobAccessValues(container, filename, context.Account, context.Key, expires)
195+ return fmt.Sprintf("%s?%s", url, values.Encode())
196+}
197+
198 type ListContainersRequest struct {
199 Marker string
200 }
201
202=== modified file 'storage_base_test.go'
203--- storage_base_test.go 2013-07-17 09:11:43 +0000
204+++ storage_base_test.go 2013-07-26 08:51:26 +0000
205@@ -161,6 +161,19 @@
206 c.Assert(observed, Equals, expected)
207 }
208
209+type TestSign struct{}
210+
211+var _ = Suite(&TestSign{})
212+
213+func (suite *TestSign) TestSign(c *C) {
214+ key := base64.StdEncoding.EncodeToString([]byte("dummykey"))
215+ signable := "a-string-to-sign"
216+
217+ observed := sign(key, signable)
218+ expected := "5j1DSsm07IEh3u9JQQd0KPwtM6pEGChzrAF7Zf/LxLc="
219+ c.Assert(observed, Equals, expected)
220+}
221+
222 type TestComposeAuthHeader struct{}
223
224 var _ = Suite(&TestComposeAuthHeader{})
225@@ -303,6 +316,31 @@
226 "http://"+url.QueryEscape(account)+".blob.core.windows.net/"+url.QueryEscape(container)+"/"+url.QueryEscape(file))
227 }
228
229+func (suite *TestStorageContext) TestGetSignedFileURL(c *C) {
230+ account := "account"
231+ container := "container"
232+ file := "/a/file"
233+ key := base64.StdEncoding.EncodeToString([]byte("dummykey"))
234+ context := StorageContext{Account: account, Key: key}
235+ expires := time.Now()
236+
237+ signedURL := context.GetAnonymousFileURL(container, file, expires)
238+ // The only difference with the non-anon URL is the query string.
239+ parsed, err := url.Parse(signedURL)
240+ c.Assert(err, IsNil)
241+ fileURL, err := url.Parse(context.GetFileURL(container, file))
242+ c.Assert(err, IsNil)
243+ c.Check(parsed.Scheme, Equals, fileURL.Scheme)
244+ c.Check(parsed.Host, Equals, fileURL.Host)
245+ c.Check(parsed.Path, Equals, fileURL.Path)
246+
247+ values, err := url.ParseQuery(parsed.RawQuery)
248+ c.Assert(err, IsNil)
249+ signature := values.Get("sig")
250+ expectedSignature := getReadBlobAccessValues(container, file, account, key, expires).Get("sig")
251+ c.Check(signature, Equals, expectedSignature)
252+}
253+
254 func (suite *TestStorageContext) TestGetClientReturnsDefaultClient(c *C) {
255 context := &StorageContext{client: nil}
256 c.Assert(context.getClient(), Equals, http.DefaultClient)

Subscribers

People subscribed via source and target branches

to all changes: