internal: refactor storage credentials (#1071)

* Move storage clients to separate packages

* Allow setting of client credentials for AWS S3

* Use managed identity client secret or default credentials for Azure Blob Storage

* Use credentials file to authorize GCS client

---------

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
Daniel Weiße 2023-03-02 15:08:31 +01:00 committed by GitHub
parent 96b4b74a7a
commit 5eb73706f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 857 additions and 1130 deletions

View file

@ -17,7 +17,6 @@ package setup
import (
"context"
"encoding/base64"
"fmt"
"net/url"
@ -26,37 +25,12 @@ import (
"github.com/edgelesssys/constellation/v2/internal/kms/kms/azure"
"github.com/edgelesssys/constellation/v2/internal/kms/kms/cluster"
"github.com/edgelesssys/constellation/v2/internal/kms/kms/gcp"
"github.com/edgelesssys/constellation/v2/internal/kms/storage"
"github.com/edgelesssys/constellation/v2/internal/kms/storage/awss3"
"github.com/edgelesssys/constellation/v2/internal/kms/storage/azureblob"
"github.com/edgelesssys/constellation/v2/internal/kms/storage/gcs"
"github.com/edgelesssys/constellation/v2/internal/kms/uri"
)
// Well known endpoints for KMS services.
const (
AWSKMSURI = "kms://aws?keyPolicy=%s&kekID=%s"
AzureKMSURI = "kms://azure?tenantID=%s&clientID=%s&clientSecret=%s&name=%s&type=%s&kekID=%s"
GCPKMSURI = "kms://gcp?project=%s&location=%s&keyRing=%s&protectionLvl=%s&kekID=%s"
ClusterKMSURI = "kms://cluster-kms?key=%s&salt=%s"
AWSS3URI = "storage://aws?bucket=%s"
AzureBlobURI = "storage://azure?container=%s&connectionString=%s"
GCPStorageURI = "storage://gcp?projects=%s&bucket=%s"
NoStoreURI = "storage://no-store"
)
// MasterSecret holds the master key and salt for deriving keys.
type MasterSecret struct {
Key []byte `json:"key"`
Salt []byte `json:"salt"`
}
// EncodeToURI returns an URI encoding the master secret.
func (m *MasterSecret) EncodeToURI() string {
return fmt.Sprintf(
ClusterKMSURI,
base64.URLEncoding.EncodeToString(m.Key),
base64.URLEncoding.EncodeToString(m.Salt),
)
}
// KMSInformation about an existing KMS.
type KMSInformation struct {
KMSURI string
@ -76,41 +50,41 @@ func KMS(ctx context.Context, storageURI, kmsURI string) (kms.CloudKMS, error) {
// getStore creates a key store depending on the given parameters.
func getStore(ctx context.Context, storageURI string) (kms.Storage, error) {
uri, err := url.Parse(storageURI)
url, err := url.Parse(storageURI)
if err != nil {
return nil, err
}
if uri.Scheme != "storage" {
return nil, fmt.Errorf("invalid storage URI: invalid scheme: %s", uri.Scheme)
if url.Scheme != "storage" {
return nil, fmt.Errorf("invalid storage URI: invalid scheme: %s", url.Scheme)
}
switch uri.Host {
switch url.Host {
case "aws":
bucket, err := getAWSS3Config(uri)
cfg, err := uri.DecodeAWSS3ConfigFromURI(storageURI)
if err != nil {
return nil, err
}
return storage.NewAWSS3Storage(ctx, bucket, nil)
return awss3.New(ctx, cfg)
case "azure":
container, connString, err := getAzureBlobConfig(uri)
cfg, err := uri.DecodeAzureBlobConfigFromURI(storageURI)
if err != nil {
return nil, err
}
return storage.NewAzureStorage(ctx, connString, container, nil)
return azureblob.New(ctx, cfg)
case "gcp":
project, bucket, err := getGCPStorageConfig(uri)
cfg, err := uri.DecodeGoogleCloudStorageConfigFromURI(storageURI)
if err != nil {
return nil, err
}
return storage.NewGoogleCloudStorage(ctx, project, bucket, nil)
return gcs.New(ctx, cfg)
case "no-store":
return nil, nil
default:
return nil, fmt.Errorf("unknown storage type: %s", uri.Host)
return nil, fmt.Errorf("unknown storage type: %s", url.Host)
}
}
@ -157,68 +131,3 @@ func getKMS(ctx context.Context, kmsURI string, store kms.Storage) (kms.CloudKMS
return nil, fmt.Errorf("unknown KMS type: %s", url.Host)
}
}
type defaultPolicyProducer struct {
policy string
}
func (p *defaultPolicyProducer) CreateKeyPolicy(keyID string) (string, error) {
return p.policy, nil
}
func getAWSS3Config(uri *url.URL) (string, error) {
r, err := getConfig(uri.Query(), []string{"bucket"})
return r[0], err
}
func getAWSKMSConfig(uri *url.URL) (*defaultPolicyProducer, string, error) {
r, err := getConfig(uri.Query(), []string{"keyPolicy", "kekID"})
if err != nil {
return nil, "", err
}
if len(r) != 2 {
return nil, "", fmt.Errorf("expected 2 KmsURI args, got %d", len(r))
}
kekID, err := base64.URLEncoding.DecodeString(r[1])
if err != nil {
return nil, "", fmt.Errorf("parsing kekID from kmsUri: %w", err)
}
return &defaultPolicyProducer{policy: r[0]}, string(kekID), err
}
func getAzureBlobConfig(uri *url.URL) (string, string, error) {
r, err := getConfig(uri.Query(), []string{"container", "connectionString"})
if err != nil {
return "", "", err
}
return r[0], r[1], nil
}
func getGCPStorageConfig(uri *url.URL) (string, string, error) {
r, err := getConfig(uri.Query(), []string{"project", "bucket"})
return r[0], r[1], err
}
// getConfig parses url query values, returning a map of the requested values.
// Returns an error if a key has no value.
// This function MUST always return a slice of the same length as len(keys).
func getConfig(values url.Values, keys []string) ([]string, error) {
res := make([]string, len(keys))
for idx, key := range keys {
val := values.Get(key)
if val == "" {
return res, fmt.Errorf("missing value for key: %q", key)
}
val, err := url.QueryUnescape(val)
if err != nil {
return res, fmt.Errorf("failed to unescape value for key: %q", key)
}
res[idx] = val
}
return res, nil
}

View file

@ -8,18 +8,13 @@ package setup
import (
"context"
"encoding/base64"
"fmt"
"net/url"
"testing"
"github.com/edgelesssys/constellation/v2/internal/kms/uri"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
)
const constellationKekID = "Constellation"
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m,
// https://github.com/census-instrumentation/opencensus-go/issues/1262
@ -27,105 +22,6 @@ func TestMain(m *testing.M) {
)
}
func TestGetStore(t *testing.T) {
testCases := map[string]struct {
uri string
wantErr bool
}{
"no store": {
uri: NoStoreURI,
wantErr: false,
},
"aws s3": {
uri: fmt.Sprintf(AWSS3URI, ""),
wantErr: true,
},
"azure blob": {
uri: fmt.Sprintf(AzureBlobURI, "", ""),
wantErr: true,
},
"gcp storage": {
uri: fmt.Sprintf(GCPStorageURI, "", ""),
wantErr: true,
},
"unknown store": {
uri: "storage://unknown",
wantErr: true,
},
"invalid scheme": {
uri: ClusterKMSURI,
wantErr: true,
},
"not a url": {
uri: ":/123",
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
_, err := getStore(context.Background(), tc.uri)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}
func TestGetKMS(t *testing.T) {
testCases := map[string]struct {
uri string
wantErr bool
}{
"cluster kms": {
uri: fmt.Sprintf(ClusterKMSURI, base64.URLEncoding.EncodeToString([]byte("key")), base64.URLEncoding.EncodeToString([]byte("salt"))),
wantErr: false,
},
"aws kms": {
uri: fmt.Sprintf(AWSKMSURI, "", ""),
wantErr: true,
},
"azure kms": {
uri: fmt.Sprintf(AzureKMSURI, "", "", "", "", "", ""),
wantErr: true,
},
"gcp kms": {
uri: fmt.Sprintf(GCPKMSURI, "", "", "", "", ""),
wantErr: true,
},
"unknown kms": {
uri: "kms://unknown",
wantErr: true,
},
"invalid scheme": {
uri: NoStoreURI,
wantErr: true,
},
"not a url": {
uri: ":/123",
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
kms, err := getKMS(context.Background(), tc.uri, nil)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.NotNil(kms)
}
})
}
}
func TestSetUpKMS(t *testing.T) {
assert := assert.New(t)
@ -133,98 +29,8 @@ func TestSetUpKMS(t *testing.T) {
assert.Error(err)
assert.Nil(kms)
masterSecret := MasterSecret{Key: []byte("key"), Salt: []byte("salt")}
masterSecret := uri.MasterSecret{Key: []byte("key"), Salt: []byte("salt")}
kms, err = KMS(context.Background(), "storage://no-store", masterSecret.EncodeToURI())
assert.NoError(err)
assert.NotNil(kms)
}
func TestGetAWSKMSConfig(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
policy := "{keyPolicy: keyPolicy}"
escapedPolicy := url.QueryEscape(policy)
kekID := base64.URLEncoding.EncodeToString([]byte(constellationKekID))
uri, err := url.Parse(fmt.Sprintf(AWSKMSURI, escapedPolicy, kekID))
require.NoError(err)
policyProducer, rKekID, err := getAWSKMSConfig(uri)
require.NoError(err)
keyPolicy, err := policyProducer.CreateKeyPolicy("")
require.NoError(err)
assert.Equal(policy, keyPolicy)
assert.Equal(constellationKekID, rKekID)
}
func TestGetAzureBlobConfig(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
connStr := "DefaultEndpointsProtocol=https;AccountName=test;AccountKey=Q29uc3RlbGxhdGlvbg==;EndpointSuffix=core.windows.net"
escapedConnStr := url.QueryEscape(connStr)
container := "test"
uri, err := url.Parse(fmt.Sprintf(AzureBlobURI, container, escapedConnStr))
require.NoError(err)
rContainer, rConnStr, err := getAzureBlobConfig(uri)
require.NoError(err)
assert.Equal(container, rContainer)
assert.Equal(connStr, rConnStr)
}
func TestGetConfig(t *testing.T) {
const testURI = "test://config?name=test-name&data=test-data&value=test-value"
testCases := map[string]struct {
uri string
keys []string
wantErr bool
}{
"success": {
uri: testURI,
keys: []string{"name", "data", "value"},
wantErr: false,
},
"less keys than capture groups": {
uri: testURI,
keys: []string{"name", "data"},
wantErr: false,
},
"invalid regex": {
uri: testURI,
keys: []string{"name", "data", "test-value"},
wantErr: true,
},
"missing value": {
uri: "test://config?name=test-name&data=test-data&value",
keys: []string{"name", "data", "value"},
wantErr: true,
},
"more keys than expected": {
uri: testURI,
keys: []string{"name", "data", "value", "anotherValue"},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
uri, err := url.Parse(tc.uri)
require.NoError(err)
res, err := getConfig(uri.Query(), tc.keys)
if tc.wantErr {
assert.Error(err)
assert.Len(res, len(tc.keys))
} else {
assert.NoError(err)
require.Len(res, len(tc.keys))
for i := range tc.keys {
assert.NotEmpty(res[i])
}
}
})
}
}