Skip to content

Commit a92403f

Browse files
committed
Add Support for SAS keys in Azure Blob
Signed-off-by: Somtochi Onyekwere <somtochionyekwere@gmail.com>
1 parent 9d6eada commit a92403f

File tree

3 files changed

+149
-0
lines changed

3 files changed

+149
-0
lines changed

docs/spec/v1beta2/buckets.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ sets of `.data` fields:
295295
- `clientId` for authenticating using a Managed Identity.
296296
- `accountKey` for authenticating using a
297297
[Shared Key](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/storage/azblob#SharedKeyCredential).
298+
- `sasKey` for authenticating using a [SAS Token](https://docs.microsoft.com/en-us/azure/storage/common/storage-sas-overview)
298299

299300
For any Managed Identity and/or Azure Active Directory authentication method,
300301
the base URL can be configured using `.data.authorityHost`. If not supplied,
@@ -432,6 +433,36 @@ data:
432433
accountKey: <BASE64>
433434
```
434435

436+
##### Azure Blob SAS Token example
437+
438+
```yaml
439+
---
440+
apiVersion: source.toolkit.fluxcd.io/v1beta2
441+
kind: Bucket
442+
metadata:
443+
name: azure-sas-token
444+
namespace: default
445+
spec:
446+
interval: 5m0s
447+
provider: azure
448+
bucketName: <bucket-name>
449+
endpoint: https://<account-name>.blob.core.windows.net
450+
secretRef:
451+
name: azure-key
452+
---
453+
apiVersion: v1
454+
kind: Secret
455+
metadata:
456+
name: azure-key
457+
namespace: default
458+
type: Opaque
459+
data:
460+
sasKey: <base64>
461+
```
462+
463+
Note that the Azure SAS Token has an expiry date and it should be updated before it expires so that Flux can
464+
continue to access Azure Storage.
465+
435466
#### GCP
436467

437468
When a Bucket's `.spec.provider` is set to `gcp`, the source-controller will

pkg/azure/blob.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const (
5252
clientCertificateSendChainField = "clientCertificateSendChain"
5353
authorityHostField = "authorityHost"
5454
accountKeyField = "accountKey"
55+
sasKeyField = "sasKey"
5556
)
5657

5758
// BlobClient is a minimal Azure Blob client for fetching objects.
@@ -104,6 +105,14 @@ func NewClient(obj *sourcev1.Bucket, secret *corev1.Secret) (c *BlobClient, err
104105
c.ServiceClient, err = azblob.NewServiceClientWithSharedKey(obj.Spec.Endpoint, cred, &azblob.ClientOptions{})
105106
return
106107
}
108+
109+
var fullPath string
110+
if fullPath, err = sasTokenFromSecret(obj.Spec.Endpoint, secret); err != nil {
111+
return
112+
}
113+
114+
c.ServiceClient, err = azblob.NewServiceClientWithNoCredential(fullPath, &azblob.ClientOptions{})
115+
return
107116
}
108117

109118
// Compose token chain based on environment.
@@ -148,6 +157,9 @@ func ValidateSecret(secret *corev1.Secret) error {
148157
if _, hasAccountKey := secret.Data[accountKeyField]; hasAccountKey {
149158
valid = true
150159
}
160+
if _, hasSasKey := secret.Data[sasKeyField]; hasSasKey {
161+
valid = true
162+
}
151163
if _, hasAuthorityHost := secret.Data[authorityHostField]; hasAuthorityHost {
152164
valid = true
153165
}
@@ -343,6 +355,34 @@ func sharedCredentialFromSecret(endpoint string, secret *corev1.Secret) (*azblob
343355
return nil, nil
344356
}
345357

358+
// sasTokenFromSecret retrieves the SAS Token from the `sasKey`. It returns an empty string if the Secret
359+
// does not contain a valid set of credentials.
360+
func sasTokenFromSecret(ep string, secret *corev1.Secret) (string, error) {
361+
if sasKey, hasSASKey := secret.Data[sasKeyField]; hasSASKey {
362+
queryString := strings.TrimPrefix(string(sasKey), "?")
363+
values, err := url.ParseQuery(queryString)
364+
if err != nil {
365+
return "", fmt.Errorf("unable to parse SAS TOKEN: %s", err)
366+
}
367+
epURL, err := url.Parse(ep)
368+
if err != nil {
369+
return "", fmt.Errorf("unable to parse endpoint url: %s", err)
370+
}
371+
372+
//merge the query values in the endpoint wuth the token
373+
epValues := epURL.Query()
374+
for key, val := range epValues {
375+
for _, str := range val {
376+
values.Set(key, str)
377+
}
378+
}
379+
380+
epURL.RawQuery = values.Encode()
381+
return epURL.String(), nil
382+
}
383+
return "", nil
384+
}
385+
346386
// chainCredentialWithSecret tries to create a set of tokens, and returns an
347387
// azidentity.ChainedTokenCredential if at least one of the following tokens was
348388
// successfully created:

pkg/azure/blob_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"errors"
2626
"fmt"
2727
"math/big"
28+
"net/url"
2829
"testing"
2930

3031
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
@@ -68,6 +69,14 @@ func TestValidateSecret(t *testing.T) {
6869
},
6970
},
7071
},
72+
{
73+
name: "valid SAS Key Secret",
74+
secret: &corev1.Secret{
75+
Data: map[string][]byte{
76+
sasKeyField: []byte("?spr=<some-sas-url"),
77+
},
78+
},
79+
},
7180
{
7281
name: "valid SharedKey Secret",
7382
secret: &corev1.Secret{
@@ -292,6 +301,75 @@ func Test_sharedCredentialFromSecret(t *testing.T) {
292301
}
293302
}
294303

304+
func Test_sasTokenFromSecret(t *testing.T) {
305+
tests := []struct {
306+
name string
307+
endpoint string
308+
secret *corev1.Secret
309+
want string
310+
wantErr bool
311+
}{
312+
{
313+
name: "Valid SAS Token",
314+
endpoint: "https://accountName.blob.windows.net",
315+
secret: &corev1.Secret{
316+
Data: map[string][]byte{
317+
sasKeyField: []byte("?sv=2020-08-0&ss=bfqt&srt=co&sp=rwdlacupitfx&se=2022-05-26T21:55:35Z&st=2022-05-26T13:55:35Z&spr=https&sig=JlHT"),
318+
},
319+
},
320+
want: "https://accountName.blob.windows.net?sv=2020-08-0&ss=bfqt&srt=co&sp=rwdlacupitfx&se=2022-05-26T21:55:35Z&st=2022-05-26T13:55:35Z&spr=https&sig=JlHT",
321+
},
322+
{
323+
name: "Valid SAS Token without leading question mark",
324+
endpoint: "https://accountName.blob.windows.net",
325+
secret: &corev1.Secret{
326+
Data: map[string][]byte{
327+
sasKeyField: []byte("sv=2020-08-04&ss=bfqt&srt=co&sp=rwdl&se=2022-05-26T21:55:35Z&st=2022-05-26&spr=https&sig=JlHT"),
328+
},
329+
},
330+
want: "https://accountName.blob.windows.net?sv=2020-08-04&ss=bfqt&srt=co&sp=rwdl&se=2022-05-26T21:55:35Z&st=2022-05-26&spr=https&sig=JlHT",
331+
},
332+
{
333+
name: "endpoint with query values",
334+
endpoint: "https://accountName.blob.windows.net?sv=2020-08-04",
335+
secret: &corev1.Secret{
336+
Data: map[string][]byte{
337+
sasKeyField: []byte("ss=bfqt&srt=co&sp=rwdl&se=2022-05-26T21:55:35Z&st=2022-05-26&spr=https&sig=JlHT"),
338+
},
339+
},
340+
want: "https://accountName.blob.windows.net?sv=2020-08-04&ss=bfqt&srt=co&sp=rwdl&se=2022-05-26T21:55:35Z&st=2022-05-26&spr=https&sig=JlHT",
341+
},
342+
{
343+
name: "invalid sas token",
344+
secret: &corev1.Secret{
345+
Data: map[string][]byte{
346+
sasKeyField: []byte("%##sssvecrpt"),
347+
},
348+
},
349+
wantErr: true,
350+
},
351+
}
352+
for _, tt := range tests {
353+
t.Run(tt.name, func(t *testing.T) {
354+
g := NewWithT(t)
355+
356+
_, err := url.ParseQuery("")
357+
got, err := sasTokenFromSecret(tt.endpoint, tt.secret)
358+
g.Expect(err != nil).To(Equal(tt.wantErr))
359+
if tt.want != "" {
360+
ttVaules, err := url.Parse(tt.want)
361+
g.Expect(err).To(BeNil())
362+
363+
gotValues, err := url.Parse(got)
364+
g.Expect(err).To(BeNil())
365+
g.Expect(gotValues.Query()).To(Equal(ttVaules.Query()))
366+
return
367+
}
368+
g.Expect(got).To(Equal(""))
369+
})
370+
}
371+
}
372+
295373
func Test_chainCredentialWithSecret(t *testing.T) {
296374
g := NewWithT(t)
297375

0 commit comments

Comments
 (0)