From 7767c5822a57a3751c40d7a8d432c2bcac720eed Mon Sep 17 00:00:00 2001 From: Ramon de Klein Date: Tue, 9 Apr 2024 00:19:24 +0200 Subject: [PATCH] Bugfixes and some code improvements --- internal/keystore/azure/client.go | 126 ++++++------------ internal/keystore/azure/error.go | 31 ++--- internal/keystore/azure/key-vault-error.go | 44 ------- internal/keystore/azure/key-vault.go | 144 ++++++++------------- internal/keystore/azure/key-vault_test.go | 113 +++++++++------- kesconf/config.go | 3 - kesconf/file.go | 24 ++-- 7 files changed, 191 insertions(+), 294 deletions(-) delete mode 100644 internal/keystore/azure/key-vault-error.go diff --git a/internal/keystore/azure/client.go b/internal/keystore/azure/client.go index d703d2e4..d0960e4b 100644 --- a/internal/keystore/azure/client.go +++ b/internal/keystore/azure/client.go @@ -8,9 +8,7 @@ import ( "context" "errors" "fmt" - "math" "net/http" - "path" "time" "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" @@ -43,17 +41,9 @@ type client struct { func (c *client) CreateSecret(ctx context.Context, name, value string) (status, error) { _, err := c.azsecretsClient.SetSecret(ctx, name, azsecrets.SetSecretParameters{ Value: &value, - }, &azsecrets.SetSecretOptions{}) + }, nil) if err != nil { - azResp, ok := transportErrToResponseError(err) - if !ok { - return status{}, err - } - return status{ - StatusCode: azResp.StatusCode, - ErrorCode: azResp.ErrorCode, - Message: azResp.errorResponse.Error.Message, - }, nil + return transportErrToStatus(err) } return status{ StatusCode: http.StatusOK, @@ -77,20 +67,13 @@ func (c *client) CreateSecret(ctx context.Context, name, value string) (status, // if the secret is disabled, expired or should not // be used, yet. func (c *client) GetSecret(ctx context.Context, name, version string) (string, status, error) { - response, err := c.azsecretsClient.GetSecret(ctx, name, version, &azsecrets.GetSecretOptions{}) + response, err := c.azsecretsClient.GetSecret(ctx, name, version, nil) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return "", status{}, err } if err != nil { - azResp, ok := transportErrToResponseError(err) - if !ok { - return "", status{}, err - } - return "", status{ - StatusCode: azResp.StatusCode, - ErrorCode: azResp.ErrorCode, - Message: azResp.errorResponse.Error.Message, - }, nil + stat, err := transportErrToStatus(err) + return "", stat, err } if response.Attributes.Enabled != nil && !*response.Attributes.Enabled { return "", status{ @@ -135,17 +118,9 @@ func (c *client) GetSecret(ctx context.Context, name, version string) (string, s // even if it returns 200 OK. Instead, the secret may be in // a transition state from "active" to (soft) deleted. func (c *client) DeleteSecret(ctx context.Context, name string) (status, error) { - _, err := c.azsecretsClient.DeleteSecret(ctx, name, &azsecrets.DeleteSecretOptions{}) + _, err := c.azsecretsClient.DeleteSecret(ctx, name, nil) if err != nil { - azResp, ok := transportErrToResponseError(err) - if !ok { - return status{}, err - } - return status{ - StatusCode: azResp.StatusCode, - ErrorCode: azResp.ErrorCode, - Message: azResp.errorResponse.Error.Message, - }, nil + return transportErrToStatus(err) } return status{ StatusCode: http.StatusOK, @@ -158,17 +133,12 @@ func (c *client) DeleteSecret(ctx context.Context, name string) (status, error) // recovered. Therefore, deleting a KeyVault secret permanently is // a two-step process. func (c *client) PurgeSecret(ctx context.Context, name string) (status, error) { - _, err := c.azsecretsClient.PurgeDeletedSecret(ctx, name, &azsecrets.PurgeDeletedSecretOptions{}) + _, err := c.azsecretsClient.PurgeDeletedSecret(ctx, name, nil) if err != nil { - azResp, ok := transportErrToResponseError(err) - if !ok { - return status{}, err + stat, err := transportErrToStatus(err) + if stat.StatusCode != http.StatusNoContent && stat.StatusCode != http.StatusOK && stat.StatusCode != http.StatusNotFound { + return stat, err } - return status{ - StatusCode: azResp.StatusCode, - ErrorCode: azResp.ErrorCode, - Message: azResp.errorResponse.Error.Message, - }, nil } return status{ StatusCode: http.StatusOK, @@ -183,53 +153,39 @@ func (c *client) PurgeSecret(ctx context.Context, name string) (status, error) { // versions of the given secret. When a secret contains more then 25 // versions GetFirstVersions returns a status with a 422 HTTP error code. func (c *client) GetFirstVersion(ctx context.Context, name string) (string, status, error) { - pager := c.azsecretsClient.NewListSecretPropertiesVersionsPager(name, &azsecrets.ListSecretPropertiesVersionsOptions{}) + pager := c.azsecretsClient.NewListSecretPropertiesVersionsPager(name, nil) + page, err := pager.NextPage(ctx) + if err != nil { + stat, err := transportErrToStatus(err) + return "", stat, err + } if pager.More() { - page, err := pager.NextPage(ctx) - if err != nil { - azResp, ok := transportErrToResponseError(err) - if !ok { - return "", status{}, err - } - return "", status{ - StatusCode: azResp.StatusCode, - ErrorCode: azResp.ErrorCode, - Message: azResp.errorResponse.Error.Message, - }, nil - } - if page.SecretPropertiesListResult.NextLink != nil && *page.SecretPropertiesListResult.NextLink != "" { - return "", status{ - StatusCode: http.StatusUnprocessableEntity, - ErrorCode: "TooManyObjectVersions", - Message: fmt.Sprintf("There are too many versions of %q.", name), - }, nil - } - if len(page.SecretPropertiesListResult.Value) == 0 { - return "", status{ - StatusCode: http.StatusNotFound, - ErrorCode: "NoObjectVersions", - Message: fmt.Sprintf("There are no versions of %q.", name), - }, nil - } - var ( - id string // most recent Secret ID - createdAt int64 = math.MaxInt64 // most recent createdAt UNIX timestamp - ) - for _, v := range page.SecretPropertiesListResult.Value { - if v.Attributes != nil && v.Attributes.Created != nil && v.ID != nil { - if createdAt > (*v.Attributes.Created).Unix() { - createdAt = (*v.Attributes.Created).Unix() - id = v.ID.Version() - } + return "", status{ + StatusCode: http.StatusUnprocessableEntity, + ErrorCode: "TooManyObjectVersions", + Message: fmt.Sprintf("There are too many versions of %q.", name), + }, nil + } + if len(page.SecretPropertiesListResult.Value) == 0 { + return "", status{ + StatusCode: http.StatusNotFound, + ErrorCode: "NoObjectVersions", + Message: fmt.Sprintf("There are no versions of %q.", name), + }, nil + } + var ( + version string // most recent Secret version + createdAt *time.Time = nil // most recent createdAt UNIX timestamp + ) + for _, v := range page.SecretPropertiesListResult.Value { + if v.Attributes != nil && v.Attributes.Created != nil && v.ID != nil { + if createdAt == nil || createdAt.After(*v.Attributes.Created) { + createdAt = v.Attributes.Created + version = v.ID.Version() } } - return path.Base(id), status{ - StatusCode: http.StatusOK, - }, nil } - return "", status{ - StatusCode: http.StatusNotFound, - ErrorCode: "NoObjectVersions", - Message: fmt.Sprintf("There are no versions of %q.", name), + return version, status{ + StatusCode: http.StatusOK, }, nil } diff --git a/internal/keystore/azure/error.go b/internal/keystore/azure/error.go index ea89121c..f215d3cf 100644 --- a/internal/keystore/azure/error.go +++ b/internal/keystore/azure/error.go @@ -6,9 +6,9 @@ package azure import ( "encoding/json" - "net/http" + "errors" - "aead.dev/mem" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" ) // errorResponse is a KeyVault secrets API error response. @@ -22,18 +22,19 @@ type errorResponse struct { } `json:"error"` } -// parseErrorResponse parses the response body as -// KeyVault secrets API error response. -func parseErrorResponse(resp *http.Response) (errorResponse, error) { - const MaxSize = 1 * mem.MiB - limit := mem.Size(resp.ContentLength) - if limit < 0 || limit > MaxSize { - limit = MaxSize +// transportErrToStatus converts a transport error to a Status. +func transportErrToStatus(err error) (status, error) { + var rerr *azcore.ResponseError + if errors.As(err, &rerr) { + var errorResponse errorResponse + if rerr.RawResponse != nil { + json.NewDecoder(rerr.RawResponse.Body).Decode(&errorResponse) + } + return status{ + ErrorCode: errorResponse.Error.Inner.Code, + StatusCode: rerr.StatusCode, + Message: errorResponse.Error.Message, + }, nil } - - var response errorResponse - if err := json.NewDecoder(mem.LimitReader(resp.Body, limit)).Decode(&response); err != nil { - return errorResponse{}, err - } - return response, nil + return status{}, err } diff --git a/internal/keystore/azure/key-vault-error.go b/internal/keystore/azure/key-vault-error.go deleted file mode 100644 index 49093d09..00000000 --- a/internal/keystore/azure/key-vault-error.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2024 - MinIO, Inc. All rights reserved. -// Use of this source code is governed by the AGPLv3 -// license that can be found in the LICENSE file. - -package azure - -import ( - "net/http" - "reflect" -) - -type responseError struct { - // ErrorCode is the error code returned by the resource provider if available. - ErrorCode string - - // StatusCode is the HTTP status code as defined in https://pkg.go.dev/net/http#pkg-constants. - StatusCode int - - // RawResponse is the underlying HTTP response. - RawResponse *http.Response - - errorResponse errorResponse -} - -// transportErrToResponseError converts a transport error to a ResponseError. -func transportErrToResponseError(terr error) (*responseError, bool) { - if reflect.TypeOf(terr).String() == "*exported.ResponseError" { - tv := reflect.ValueOf(terr).Elem() - ErrorCode := tv.FieldByName("ErrorCode").String() - StatusCode := int(tv.FieldByName("StatusCode").Int()) - RawResponse, ok := tv.FieldByName("RawResponse").Interface().(*http.Response) - var errorResponse errorResponse - if ok { - errorResponse, _ = parseErrorResponse(RawResponse) - } - return &responseError{ - ErrorCode: ErrorCode, - StatusCode: StatusCode, - RawResponse: RawResponse, - errorResponse: errorResponse, - }, true - } - return nil, false -} diff --git a/internal/keystore/azure/key-vault.go b/internal/keystore/azure/key-vault.go index 77fafdbb..e63c293e 100644 --- a/internal/keystore/azure/key-vault.go +++ b/internal/keystore/azure/key-vault.go @@ -10,12 +10,10 @@ import ( "fmt" "math/rand" "net/http" - "os" "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" - "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" "github.com/minio/kes" "github.com/minio/kes/internal/keystore" @@ -47,6 +45,11 @@ type Store struct { func (s *Store) String() string { return "Azure KeyVault: " + s.endpoint } +const ( + delay = 200 * time.Millisecond + jitter = 800 * time.Millisecond +) + // Status returns the current state of the Azure KeyVault instance. // In particular, whether it is reachable and the network latency. func (s *Store) Status(ctx context.Context) (kes.KeyStoreState, error) { @@ -110,24 +113,19 @@ func (s *Store) Create(ctx context.Context, name string, value []byte) error { if err != nil { return fmt.Errorf("azure: failed to create '%s': %v", name, err) } - if stat.StatusCode == http.StatusConflict && stat.ErrorCode == "ObjectIsDeletedButRecoverable" { - stat, err = s.client.PurgeSecret(ctx, name) + if stat.StatusCode == http.StatusConflict && (stat.ErrorCode == "ObjectIsDeletedButRecoverable" || stat.ErrorCode == "ObjectIsBeingDeleted") { + stat, err = s.purgeWithRetry(ctx, name, 25) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return err } if err != nil { return fmt.Errorf("azure: failed to create '%s': failed to purge deleted secret: %v", name, err) } - if stat.StatusCode != http.StatusNoContent { + if stat.StatusCode != http.StatusOK { return fmt.Errorf("azure: failed to create '%s': failed to purge deleted secret: %s (%s)", name, stat.Message, stat.ErrorCode) } - const ( - Retry = 7 - Delay = 200 * time.Millisecond - Jitter = 800 * time.Millisecond - ) - for i := 0; i < Retry; i++ { + for i := 0; i < 7; i++ { stat, err = s.client.CreateSecret(ctx, name, string(value)) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return err @@ -136,7 +134,7 @@ func (s *Store) Create(ctx context.Context, name string, value []byte) error { return fmt.Errorf("azure: failed to create '%s': %v", name, err) } if stat.StatusCode == http.StatusConflict && stat.ErrorCode == "ObjectIsBeingDeleted" { - time.Sleep(Delay + time.Duration(rand.Int63n(Jitter.Milliseconds()))*time.Millisecond) + time.Sleep(delay + time.Duration(rand.Int63n(jitter.Milliseconds()))*time.Millisecond) continue } break @@ -219,51 +217,23 @@ func (s *Store) Delete(ctx context.Context, name string) error { if err != nil { return fmt.Errorf("azure: failed to delete '%s': %v", name, err) } - if stat.StatusCode != http.StatusNotFound { - return kesdk.ErrKeyNotFound - } if stat.StatusCode != http.StatusOK && stat.StatusCode != http.StatusNotFound { return fmt.Errorf("azure: failed to delete '%s': %s (%s)", name, stat.Message, stat.ErrorCode) } - // Now, the key either does not exist, is being deleted or - // has been deleted. If the key does not exist then purging - // it will result in a 404 NotFound. - // If the key has been marked as deleted then purging it - // should succeed with 204 NoContent. - // However, if the key is not ready to be purged then we - // retry purging the key a couple of times - hoping that - // KeyVault completes the soft-delete process. - const ( - Retry = 7 - Delay = 200 * time.Millisecond - Jitter = 800 * time.Millisecond - ) - for i := 0; i < Retry; i++ { - stat, err = s.client.PurgeSecret(ctx, name) - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return err - } - if err != nil { - return fmt.Errorf("azure: failed to delete '%s': %s (%s)", name, stat.Message, stat.ErrorCode) - } - switch { - case stat.StatusCode == http.StatusNoContent: - return nil - case stat.StatusCode == http.StatusNotFound: - return nil - case stat.StatusCode == http.StatusForbidden && stat.ErrorCode == "ForbiddenByPolicy": - return nil - case stat.StatusCode == http.StatusConflict && stat.ErrorCode == "ObjectIsBeingDeleted": - time.Sleep(Delay + time.Duration(rand.Int63n(Jitter.Milliseconds()))*time.Millisecond) - continue - } - break + stat, err = s.purgeWithRetry(ctx, name, 10) + if err != nil { + return err } - if stat.StatusCode == http.StatusConflict && stat.ErrorCode == "ObjectIsBeingDeleted" { + + switch { + case stat.StatusCode == http.StatusOK: + return nil + case stat.StatusCode == http.StatusConflict && stat.ErrorCode == "ObjectIsBeingDeleted": return nil + default: + return fmt.Errorf("azure: failed to delete '%s': failed to purge deleted secret: %s (%s)", name, stat.Message, stat.ErrorCode) } - return fmt.Errorf("azure: failed to delete '%s': failed to purge deleted secret: %s (%s)", name, stat.Message, stat.ErrorCode) } // Get returns the first resp. oldest version of the secret. @@ -321,11 +291,11 @@ func (s *Store) List(ctx context.Context, prefix string, n int) ([]string, strin if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return nil, "", err } - azResp, ok := transportErrToResponseError(err) - if !ok { + stat, err := transportErrToStatus(err) + if err != nil { return nil, "", err } - return nil, "", fmt.Errorf("azure: failed to list keys: %s (%s)", azResp.ErrorCode, azResp.errorResponse.Error.Message) + return nil, "", fmt.Errorf("azure: failed to list keys: %s (%s)", stat.ErrorCode, stat.Message) } for _, v := range page.SecretPropertiesListResult.Value { if v.ID != nil { @@ -342,16 +312,8 @@ func (s *Store) List(ctx context.Context, prefix string, n int) ([]string, strin // Close closes the Store. func (s *Store) Close() error { return nil } -// ConnectWithCredentials tries to establish a connection to a Azure KeyVault -// instance using Azure client credentials. -func ConnectWithCredentials(_ context.Context, endpoint string, creds Credentials) (*Store, error) { - os.Setenv("AZURE_CLIENT_ID", creds.ClientID) - os.Setenv("AZURE_CLIENT_SECRET", creds.Secret) - os.Setenv("AZURE_TENANT_ID", creds.TenantID) - cred, err := azidentity.NewDefaultAzureCredential(nil) - if err != nil { - return nil, fmt.Errorf("azure: failed to create default Azure credential: %v", err) - } +// ConnectWithCredentials tries to establish a connection to an Azure KeyVault instance +func ConnectWithCredentials(endpoint string, cred azcore.TokenCredential) (*Store, error) { azsecretsClient, err := azsecrets.NewClient(endpoint, cred, &azsecrets.ClientOptions{ ClientOptions: azcore.ClientOptions{ Retry: policy.RetryOptions{ @@ -372,31 +334,35 @@ func ConnectWithCredentials(_ context.Context, endpoint string, creds Credential }, nil } -// ConnectWithIdentity tries to establish a connection to a Azure KeyVault -// instance using an Azure managed identity. -func ConnectWithIdentity(_ context.Context, endpoint string, msi ManagedIdentity) (*Store, error) { - cred, err := azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ - ID: azidentity.ClientID(msi.ClientID), - }) - if err != nil { - return nil, fmt.Errorf("azure: failed to create default Azure credential: %v", err) - } - azsecretsClient, err := azsecrets.NewClient(endpoint, cred, &azsecrets.ClientOptions{ - ClientOptions: azcore.ClientOptions{ - Retry: policy.RetryOptions{ - MaxRetries: 7, - RetryDelay: 200 * time.Millisecond, - MaxRetryDelay: 800 * time.Millisecond, - }, - }, - }) - if err != nil { - return nil, fmt.Errorf("azure: failed to create secrets client: %v", err) +func (s *Store) purgeWithRetry(ctx context.Context, name string, retries int) (status, error) { + // Now, the key either does not exist, is being deleted or + // has been deleted. If the key does not exist then purging + // it will result in a 404 NotFound. + // If the key has been marked as deleted then purging it + // should succeed with 204 NoContent. + // However, if the key is not ready to be purged then we + // retry purging the key a couple of times - hoping that + // KeyVault completes the soft-delete process. + var stat status + var err error + for i := 0; i < retries; i++ { + stat, err = s.client.PurgeSecret(ctx, name) + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return stat, err + } + if err != nil { + return stat, fmt.Errorf("azure: failed to delete '%s': %s (%s)", name, stat.Message, stat.ErrorCode) + } + switch { + case stat.StatusCode == http.StatusOK: + return stat, nil + case stat.StatusCode == http.StatusForbidden && stat.ErrorCode == "ForbiddenByPolicy": + return stat, nil + case stat.StatusCode == http.StatusConflict && stat.ErrorCode == "ObjectIsBeingDeleted": + time.Sleep(delay + time.Duration(rand.Int63n(jitter.Milliseconds()))*time.Millisecond) + continue + } + break } - return &Store{ - endpoint: endpoint, - client: client{ - azsecretsClient: azsecretsClient, - }, - }, nil + return stat, err } diff --git a/internal/keystore/azure/key-vault_test.go b/internal/keystore/azure/key-vault_test.go index 2b36dfe8..c95cdef4 100644 --- a/internal/keystore/azure/key-vault_test.go +++ b/internal/keystore/azure/key-vault_test.go @@ -6,70 +6,89 @@ package azure import ( "context" + "fmt" + "math/rand" "os" "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" +) + +var ( + prefix = fmt.Sprintf("%04d-", rand.Intn(10000)) + keyName = fmt.Sprintf("%skey", prefix) ) func TestConnectWithCredentials(t *testing.T) { - // endpoint EndPoint := os.Getenv("EndPoint") - // default credential - ClientID := os.Getenv("ClientID") - TenantID := os.Getenv("TenantID") - Secret := os.Getenv("Secret") - if ClientID == "" || TenantID == "" || Secret == "" || EndPoint == "" { - t.Skip("Skipping test due to missing credentials") + if EndPoint == "" { + t.Skip("Skipping test due to missing Keyvault endpoint") } - ctx := context.Background() - c1, err := ConnectWithCredentials(ctx, EndPoint, Credentials{TenantID: TenantID, ClientID: ClientID, Secret: Secret}) + + c, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + t.Fatalf("unable to determine Azure credentials: %v", err) + } + + c1, err := ConnectWithCredentials(EndPoint, c) if err != nil { return } - { - // delete first - _ = c1.Delete(ctx, "mytestFirst-c1") - // create - err = c1.Create(ctx, "mytestFirst-c1", []byte("hello")) - if err != nil { - t.Error(err) - } - data, err := c1.Get(ctx, "mytestFirst-c1") - t.Logf("data:[%s] err:[%v]\n", data, err) - list, s, err := c1.List(ctx, "", 25) - t.Logf("list:[%s] s:[%s] err:[%v]\n", list, s, err) - t.Log("-------------------------------") + ctx := context.Background() + + // create key + keyValue := time.Now().Format(time.RFC3339Nano) + err = c1.Create(ctx, keyName, []byte(keyValue)) + if err != nil { + t.Error(err) } - _ = c1 -} -func TestConnectWithManagedIdentityCredentials(t *testing.T) { - // endpoint - EndPoint := os.Getenv("EndPoint") - // managed identity credential - ManagedIdentityClientID := os.Getenv("ManagedIdentityClientID") - if ManagedIdentityClientID == "" || EndPoint == "" { - t.Skip("Skipping test due to missing credentials") + // delete key upon termination + defer c1.Delete(ctx, keyName) + + // fetch key and check if the value is correct + data, err := c1.Get(ctx, keyName) + if err != nil { + t.Errorf("Error fetching key: %v", err) } - ctx := context.Background() - c1, err := ConnectWithIdentity(ctx, EndPoint, ManagedIdentity{ClientID: ManagedIdentityClientID}) + if string(data) != keyValue { + t.Errorf("Got %q, but expected %q", string(data), keyValue) + } + + // list keys + list, next, err := c1.List(ctx, prefix, 25) if err != nil { - return + t.Errorf("Error listing keys: %v", err) } - { - // delete first - _ = c1.Delete(ctx, "mytestFirst-c1-m") - // create - err = c1.Create(ctx, "mytestFirst-c1-m", []byte("hello")) - if err != nil { - t.Error(err) + if len(list) != 1 || next != "" { + t.Log("Got the following keys:\n") + for _, key := range list { + t.Logf("- %s", key) + t.Errorf("Got %d keys, but expected 1 key", len(list)) } - data, err := c1.Get(ctx, "mytestFirst-c1-m") - t.Logf("data:[%s] err:[%v]\n", data, err) + } + + // delete the key + err = c1.Delete(ctx, keyName) + if err != nil { + t.Errorf("Error deleting key: %v", err) + } - list, s, err := c1.List(ctx, "", 25) - t.Logf("list:[%s] s:[%s] err:[%v]\n", list, s, err) - t.Log("-------------------------------") + // recreate the key (deleted secret should be purged automatically) + keyValue = time.Now().Format(time.RFC3339Nano) + err = c1.Create(ctx, keyName, []byte(keyValue)) + if err != nil { + t.Error(err) + } + + // fetch key and check if the value is correct + data, err = c1.Get(ctx, keyName) + if err != nil { + t.Errorf("Error fetching key: %v", err) + } + if string(data) != keyValue { + t.Errorf("Got %q, but expected %q", string(data), keyValue) } - _ = c1 } diff --git a/kesconf/config.go b/kesconf/config.go index 37c4e122..d9940f43 100644 --- a/kesconf/config.go +++ b/kesconf/config.go @@ -595,9 +595,6 @@ func ymlToKeyStore(y *ymlFile) (KeyStore, error) { if y.KeyStore.Azure.KeyVault.Endpoint.Value == "" { return nil, errors.New("kesconf: invalid Azure keyvault keystore: no endpoint specified") } - if y.KeyStore.Azure.KeyVault.Credentials == nil && y.KeyStore.Azure.KeyVault.ManagedIdentity == nil { - return nil, errors.New("kesconf: invalid Azure keyvault keystore: no authentication method specified") - } if y.KeyStore.Azure.KeyVault.Credentials != nil && y.KeyStore.Azure.KeyVault.ManagedIdentity != nil { return nil, errors.New("kesconf: invalid Azure keyvault keystore: more than one authentication method specified") } diff --git a/kesconf/file.go b/kesconf/file.go index 9dea2fdd..074c685a 100644 --- a/kesconf/file.go +++ b/kesconf/file.go @@ -16,6 +16,8 @@ import ( "slices" "time" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/minio/kes" "github.com/minio/kes/internal/https" "github.com/minio/kes/internal/keystore/aws" @@ -760,22 +762,22 @@ func (s *AzureKeyVaultKeyStore) Connect(ctx context.Context) (kes.KeyStore, erro if (s.TenantID != "" || s.ClientID != "" || s.ClientSecret != "") && s.ManagedIdentityClientID != "" { return nil, errors.New("edge: failed to connect to Azure KeyVault: more than one authentication method specified") } + var cred azcore.TokenCredential + var err error switch { case s.TenantID != "" || s.ClientID != "" || s.ClientSecret != "": - creds := azure.Credentials{ - TenantID: s.TenantID, - ClientID: s.ClientID, - Secret: s.ClientSecret, - } - return azure.ConnectWithCredentials(ctx, s.Endpoint, creds) + cred, err = azidentity.NewClientSecretCredential(s.TenantID, s.ClientID, s.ClientSecret, nil) case s.ManagedIdentityClientID != "": - creds := azure.ManagedIdentity{ - ClientID: s.ManagedIdentityClientID, - } - return azure.ConnectWithIdentity(ctx, s.Endpoint, creds) + cred, err = azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ + ID: azidentity.ClientID(s.ManagedIdentityClientID), + }) default: - return nil, errors.New("edge: failed to connect to Azure KeyVault: no authentication method specified") + cred, err = azidentity.NewDefaultAzureCredential(nil) + } + if err != nil { + return nil, fmt.Errorf("azure: failed to create default Azure credential: %v", err) } + return azure.ConnectWithCredentials(s.Endpoint, cred) } // EntrustKeyControlKeyStore is a structure containing the