Skip to content

Commit

Permalink
Add Support for SAS keys in Azure Blob
Browse files Browse the repository at this point in the history
Signed-off-by: Somtochi Onyekwere <somtochionyekwere@gmail.com>
  • Loading branch information
somtochiama committed Jun 29, 2022
1 parent baf7988 commit 0670db7
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 0 deletions.
31 changes: 31 additions & 0 deletions docs/spec/v1beta2/buckets.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ sets of `.data` fields:
- `clientId` for authenticating using a Managed Identity.
- `accountKey` for authenticating using a
[Shared Key](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/storage/azblob#SharedKeyCredential).
- `sasKey` for authenticating using a [SAS Token](https://docs.microsoft.com/en-us/azure/storage/common/storage-sas-overview)

For any Managed Identity and/or Azure Active Directory authentication method,
the base URL can be configured using `.data.authorityHost`. If not supplied,
Expand Down Expand Up @@ -504,6 +505,36 @@ spec:
endpoint: https://testfluxsas.blob.core.windows.net
```

##### Azure Blob SAS Token example

```yaml
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: Bucket
metadata:
name: azure-sas-token
namespace: default
spec:
interval: 5m0s
provider: azure
bucketName: <bucket-name>
endpoint: https://<account-name>.blob.core.windows.net
secretRef:
name: azure-key
---
apiVersion: v1
kind: Secret
metadata:
name: azure-key
namespace: default
type: Opaque
data:
sasKey: <base64>
```

Note that the Azure SAS Token has an expiry date and it should be updated before it expires so that Flux can
continue to access Azure Storage.

#### GCP

When a Bucket's `.spec.provider` is set to `gcp`, the source-controller will
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ require (
github.com/fluxcd/pkg/testserver v0.2.0
github.com/fluxcd/pkg/untar v0.1.0
github.com/fluxcd/pkg/version v0.1.0
github.com/fluxcd/pkg/masktoken v0.0.1
github.com/fluxcd/source-controller/api v0.25.8
github.com/go-git/go-billy/v5 v5.3.1
github.com/go-git/go-git/v5 v5.4.2
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,8 @@ github.com/fluxcd/pkg/helmtestserver v0.7.4 h1:/Xj2+XLz7wr38MI3uPYvVAsZB9wQOq6rp
github.com/fluxcd/pkg/helmtestserver v0.7.4/go.mod h1:aL5V4o8wUOMqeHMfjbVHS057E3ejzHMRVMqEbsK9FUQ=
github.com/fluxcd/pkg/lockedfile v0.1.0 h1:YsYFAkd6wawMCcD74ikadAKXA4s2sukdxrn7w8RB5eo=
github.com/fluxcd/pkg/lockedfile v0.1.0/go.mod h1:EJLan8t9MiOcgTs8+puDjbE6I/KAfHbdvIy9VUgIjm8=
github.com/fluxcd/pkg/masktoken v0.0.1 h1:egWR/ibTzf4L3PxE8TauKO1srD1Ye/aalgQRQuKKRdU=
github.com/fluxcd/pkg/masktoken v0.0.1/go.mod h1:sQmMtX4s5RwdGlByJazzNasWFFgBdmtNcgeZcGBI72Y=
github.com/fluxcd/pkg/runtime v0.16.2 h1:CexfMmJK+r12sHTvKWyAax0pcPomjd6VnaHXcxjUrRY=
github.com/fluxcd/pkg/runtime v0.16.2/go.mod h1:OHSKsrO+T+Ym8WZRS2oidrnauWRARuE2nfm8ewevm7M=
github.com/fluxcd/pkg/ssh v0.5.0 h1:jE9F2XvUXC2mgseeXMATvO014fLqdB30/VzlPLKsk20=
Expand Down
46 changes: 46 additions & 0 deletions pkg/azure/blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
corev1 "k8s.io/api/core/v1"
ctrl "sigs.k8s.io/controller-runtime"

"github.com/fluxcd/pkg/masktoken"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
)

Expand All @@ -52,6 +53,7 @@ const (
clientCertificateSendChainField = "clientCertificateSendChain"
authorityHostField = "authorityHost"
accountKeyField = "accountKey"
sasKeyField = "sasKey"
)

// BlobClient is a minimal Azure Blob client for fetching objects.
Expand Down Expand Up @@ -104,6 +106,14 @@ func NewClient(obj *sourcev1.Bucket, secret *corev1.Secret) (c *BlobClient, err
c.ServiceClient, err = azblob.NewServiceClientWithSharedKey(obj.Spec.Endpoint, cred, &azblob.ClientOptions{})
return
}

var fullPath string
if fullPath, err = sasTokenFromSecret(obj.Spec.Endpoint, secret); err != nil {
return
}

c.ServiceClient, err = azblob.NewServiceClientWithNoCredential(fullPath, &azblob.ClientOptions{})
return
}

// Compose token chain based on environment.
Expand Down Expand Up @@ -148,6 +158,9 @@ func ValidateSecret(secret *corev1.Secret) error {
if _, hasAccountKey := secret.Data[accountKeyField]; hasAccountKey {
valid = true
}
if _, hasSasKey := secret.Data[sasKeyField]; hasSasKey {
valid = true
}
if _, hasAuthorityHost := secret.Data[authorityHostField]; hasAuthorityHost {
valid = true
}
Expand Down Expand Up @@ -343,6 +356,39 @@ func sharedCredentialFromSecret(endpoint string, secret *corev1.Secret) (*azblob
return nil, nil
}

// sasTokenFromSecret retrieves the SAS Token from the `sasKey`. It returns an empty string if the Secret
// does not contain a valid set of credentials.
func sasTokenFromSecret(ep string, secret *corev1.Secret) (string, error) {
if sasKey, hasSASKey := secret.Data[sasKeyField]; hasSASKey {
queryString := strings.TrimPrefix(string(sasKey), "?")
values, err := url.ParseQuery(queryString)
if err != nil {
maskedErrorString, maskErr := masktoken.MaskTokenFromString(err.Error(), string(sasKey))
if maskErr != nil {
return "", fmt.Errorf("error redacting token from error message: %s", maskErr)
}
return "", fmt.Errorf("unable to parse SAS token: %s", maskedErrorString)
}

epURL, err := url.Parse(ep)
if err != nil {
return "", fmt.Errorf("unable to parse endpoint URL: %s", err)
}

//merge the query values in the endpoint wuth the token
epValues := epURL.Query()
for key, val := range epValues {
for _, str := range val {
values.Set(key, str)
}
}

epURL.RawQuery = values.Encode()
return epURL.String(), nil
}
return "", nil
}

// chainCredentialWithSecret tries to create a set of tokens, and returns an
// azidentity.ChainedTokenCredential if at least one of the following tokens was
// successfully created:
Expand Down
78 changes: 78 additions & 0 deletions pkg/azure/blob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"errors"
"fmt"
"math/big"
"net/url"
"testing"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
Expand Down Expand Up @@ -68,6 +69,14 @@ func TestValidateSecret(t *testing.T) {
},
},
},
{
name: "valid SAS Key Secret",
secret: &corev1.Secret{
Data: map[string][]byte{
sasKeyField: []byte("?spr=<some-sas-url"),
},
},
},
{
name: "valid SharedKey Secret",
secret: &corev1.Secret{
Expand Down Expand Up @@ -292,6 +301,75 @@ func Test_sharedCredentialFromSecret(t *testing.T) {
}
}

func Test_sasTokenFromSecret(t *testing.T) {
tests := []struct {
name string
endpoint string
secret *corev1.Secret
want string
wantErr bool
}{
{
name: "Valid SAS Token",
endpoint: "https://accountName.blob.windows.net",
secret: &corev1.Secret{
Data: map[string][]byte{
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"),
},
},
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",
},
{
name: "Valid SAS Token without leading question mark",
endpoint: "https://accountName.blob.windows.net",
secret: &corev1.Secret{
Data: map[string][]byte{
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"),
},
},
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",
},
{
name: "endpoint with query values",
endpoint: "https://accountName.blob.windows.net?sv=2020-08-04",
secret: &corev1.Secret{
Data: map[string][]byte{
sasKeyField: []byte("ss=bfqt&srt=co&sp=rwdl&se=2022-05-26T21:55:35Z&st=2022-05-26&spr=https&sig=JlHT"),
},
},
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",
},
{
name: "invalid sas token",
secret: &corev1.Secret{
Data: map[string][]byte{
sasKeyField: []byte("%##sssvecrpt"),
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)

_, err := url.ParseQuery("")
got, err := sasTokenFromSecret(tt.endpoint, tt.secret)
g.Expect(err != nil).To(Equal(tt.wantErr))
if tt.want != "" {
ttVaules, err := url.Parse(tt.want)
g.Expect(err).To(BeNil())

gotValues, err := url.Parse(got)
g.Expect(err).To(BeNil())
g.Expect(gotValues.Query()).To(Equal(ttVaules.Query()))
return
}
g.Expect(got).To(Equal(""))
})
}
}

func Test_chainCredentialWithSecret(t *testing.T) {
g := NewWithT(t)

Expand Down

0 comments on commit 0670db7

Please sign in to comment.