From 935681cfa135e32d1f6957705de6432e58e6f2d3 Mon Sep 17 00:00:00 2001 From: Josh Duffney Date: Tue, 26 Nov 2024 08:55:03 -0600 Subject: [PATCH] feat: nversion support for azkv --- .../azurekeyvault/provider.go | 151 +++++--- .../azurekeyvault/provider_test.go | 363 ++++++++++++++---- .../azurekeyvault/types/types.go | 2 + 3 files changed, 389 insertions(+), 127 deletions(-) diff --git a/pkg/keymanagementprovider/azurekeyvault/provider.go b/pkg/keymanagementprovider/azurekeyvault/provider.go index 5a77692d5..b3f58ab0c 100644 --- a/pkg/keymanagementprovider/azurekeyvault/provider.go +++ b/pkg/keymanagementprovider/azurekeyvault/provider.go @@ -43,6 +43,7 @@ import ( "golang.org/x/crypto/pkcs12" "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azcertificates" "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys" @@ -50,9 +51,10 @@ import ( ) const ( - ProviderName string = "azurekeyvault" - PKCS12ContentType string = "application/x-pkcs12" - PEMContentType string = "application/x-pem-file" + ProviderName string = "azurekeyvault" + PKCS12ContentType string = "application/x-pkcs12" + PEMContentType string = "application/x-pem-file" + versionHistoryLimitDefault int = 1 ) var logOpt = logger.Option{ @@ -88,6 +90,8 @@ type akvKMProviderFactory struct{} type keyKVClient interface { // GetKey retrieves a key from the keyvault GetKey(ctx context.Context, keyName string, keyVersion string) (azkeys.GetKeyResponse, error) + // NewListKeyVersionsPager retrieves a pager for listing key versions + NewListKeyVersionsPager(name string, options *azkeys.ListKeyVersionsOptions) *runtime.Pager[azkeys.ListKeyVersionsResponse] } type secretKVClient interface { // GetSecret retrieves a secret from the keyvault @@ -96,6 +100,8 @@ type secretKVClient interface { type certificateKVClient interface { // GetCertificate retrieves a certificate from the keyvault GetCertificate(ctx context.Context, certificateName string, certificateVersion string) (azcertificates.GetCertificateResponse, error) + // NewListCertificateVersionsPager creates a new instance of the ListCertificateVersionsPager + NewListCertificateVersionsPager(certificateName string, options *azcertificates.ListCertificateVersionsOptions) *runtime.Pager[azcertificates.ListCertificateVersionsResponse] } type keyKVClientImpl struct { @@ -113,11 +119,20 @@ func (c *certificateKVClientImpl) GetCertificate(ctx context.Context, certificat return c.Client.GetCertificate(ctx, certificateName, certificateVersion, nil) } +// NewListCertificateVersionsPager retrieves a pager for listing certificate versions +func (c *certificateKVClientImpl) NewListCertificateVersionsPager(certificateName string, options *azcertificates.ListCertificateVersionsOptions) *runtime.Pager[azcertificates.ListCertificateVersionsResponse] { + return c.Client.NewListCertificateVersionsPager(certificateName, options) +} + // GetKey retrieves a key from the keyvault func (c *keyKVClientImpl) GetKey(ctx context.Context, keyName string, keyVersion string) (azkeys.GetKeyResponse, error) { return c.Client.GetKey(ctx, keyName, keyVersion, nil) } +func (c *keyKVClientImpl) NewListKeyVersionsPager(name string, options *azkeys.ListKeyVersionsOptions) *runtime.Pager[azkeys.ListKeyVersionsResponse] { + return c.Client.NewListKeyVersionsPager(name, options) +} + // GetSecret retrieves a secret from the keyvault func (c *secretKVClientImpl) GetSecret(ctx context.Context, secretName string, secretVersion string) (azsecrets.GetSecretResponse, error) { return c.Client.GetSecret(ctx, secretName, secretVersion, nil) @@ -186,39 +201,51 @@ func (s *akvKMProvider) GetCertificates(ctx context.Context) (map[keymanagementp logger.GetLogger(ctx, logOpt).Debugf("fetching secret from key vault, certName %v, certVersion %v, vaultURI: %v", keyVaultCert.Name, keyVaultCert.Version, s.vaultURI) startTime := time.Now() - secretResponse, err := s.secretKVClient.GetSecret(ctx, keyVaultCert.Name, keyVaultCert.Version) - if err != nil { - if isSecretDisabledError(err) { - // if secret is disabled, get the version of the certificate for status - certResponse, err := s.certificateKVClient.GetCertificate(ctx, keyVaultCert.Name, keyVaultCert.Version) - if err != nil { - return nil, nil, fmt.Errorf("failed to get certificate objectName:%s, objectVersion:%s, error: %w", keyVaultCert.Name, keyVaultCert.Version, err) - } - certBundle := certResponse.CertificateBundle - keyVaultCert.Version = getObjectVersion(*certBundle.KID) - isEnabled := *certBundle.Attributes.Enabled - lastRefreshed := startTime.Format(time.RFC3339) - certProperty := getStatusProperty(keyVaultCert.Name, keyVaultCert.Version, lastRefreshed, isEnabled) - certsStatus = append(certsStatus, certProperty) - mapKey := keymanagementprovider.KMPMapKey{Name: keyVaultCert.Name, Version: keyVaultCert.Version, Enabled: isEnabled} - keymanagementprovider.DeleteCertificateFromMap(s.resource, mapKey) - continue + // if versionHistoryLimit is not set, set it to default value 1 + if keyVaultCert.VersionHistoryLimit == 0 { + keyVaultCert.VersionHistoryLimit = versionHistoryLimitDefault + } + + versionHistory := []string{} + certVersionPager := s.certificateKVClient.NewListCertificateVersionsPager(keyVaultCert.Name, nil) + for certVersionPager.More() { + pager, err := certVersionPager.NextPage(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get certificate versions for objectName:%s, error: %w", keyVaultCert.Name, err) + } + for _, cert := range pager.Value { + versionHistory = append(versionHistory, cert.ID.Version()) } - return nil, nil, fmt.Errorf("failed to get secret objectName:%s, objectVersion:%s, error: %w", keyVaultCert.Name, keyVaultCert.Version, err) } - secretBundle := secretResponse.SecretBundle - isEnabled := *secretBundle.Attributes.Enabled + for _, version := range versionHistory[:keyVaultCert.VersionHistoryLimit] { + secretReponse, err := s.secretKVClient.GetSecret(ctx, keyVaultCert.Name, version) + if err != nil { + if isSecretDisabledError(err) { + isEnabled := false + lastRefreshed := startTime.Format(time.RFC3339) + certProperty := getStatusProperty(keyVaultCert.Name, version, lastRefreshed, isEnabled) + certsStatus = append(certsStatus, certProperty) + mapKey := keymanagementprovider.KMPMapKey{Name: keyVaultCert.Name, Version: version, Enabled: isEnabled} + keymanagementprovider.DeleteCertificateFromMap(s.resource, mapKey) + continue + } + return nil, nil, fmt.Errorf("failed to get secret objectName:%s, objectVersion:%s, error: %w", keyVaultCert.Name, version, err) + } - certResult, certProperty, err := getCertsFromSecretBundle(ctx, secretBundle, keyVaultCert.Name, isEnabled) - if err != nil { - return nil, nil, fmt.Errorf("failed to get certificates from secret bundle:%w", err) - } + secretBundle := secretReponse.SecretBundle + isEnabled := *secretBundle.Attributes.Enabled - metrics.ReportAKVCertificateDuration(ctx, time.Since(startTime).Milliseconds(), keyVaultCert.Name) - certsStatus = append(certsStatus, certProperty...) - certMapKey := keymanagementprovider.KMPMapKey{Name: keyVaultCert.Name, Version: keyVaultCert.Version, Enabled: isEnabled} - certsMap[certMapKey] = certResult + certResult, certProperty, err := getCertsFromSecretBundle(ctx, secretBundle, keyVaultCert.Name, isEnabled) + if err != nil { + return nil, nil, fmt.Errorf("failed to get certificates from secret bundle:%w", err) + } + + metrics.ReportAKVCertificateDuration(ctx, time.Since(startTime).Milliseconds(), keyVaultCert.Name) + certsStatus = append(certsStatus, certProperty...) + certMapKey := keymanagementprovider.KMPMapKey{Name: keyVaultCert.Name, Version: version, Enabled: isEnabled} + certsMap[certMapKey] = certResult + } } return certsMap, getStatusMap(certsStatus, types.CertificatesStatus), nil } @@ -233,33 +260,49 @@ func (s *akvKMProvider) GetKeys(ctx context.Context) (map[keymanagementprovider. // fetch the key object from Key Vault startTime := time.Now() - keyResponse, err := s.keyKVClient.GetKey(ctx, keyVaultKey.Name, keyVaultKey.Version) - if err != nil { - return nil, nil, fmt.Errorf("failed to get key objectName:%s, objectVersion:%s, error: %w", keyVaultKey.Name, keyVaultKey.Version, err) + // if versionHistoryLimit is not set, set it to default value 1 + if keyVaultKey.VersionHistoryLimit == 0 { + keyVaultKey.VersionHistoryLimit = versionHistoryLimitDefault } - keyBundle := keyResponse.KeyBundle - isEnabled := *keyBundle.Attributes.Enabled - // if version is set as "" in the config, use the version from the key bundle - keyVaultKey.Version = getObjectVersion(string(*keyBundle.Key.KID)) - - if !isEnabled { - startTime := time.Now() - lastRefreshed := startTime.Format(time.RFC3339) - properties := getStatusProperty(keyVaultKey.Name, keyVaultKey.Version, lastRefreshed, isEnabled) - keysStatus = append(keysStatus, properties) - mapKey := keymanagementprovider.KMPMapKey{Name: keyVaultKey.Name, Version: keyVaultKey.Version, Enabled: isEnabled} - keymanagementprovider.DeleteKeyFromMap(s.resource, mapKey) - continue + + versionHistory := []string{} + keyVersionPager := s.keyKVClient.NewListKeyVersionsPager(keyVaultKey.Name, nil) + for keyVersionPager.More() { + pager, err := keyVersionPager.NextPage(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to get key versions for objectName:%s, error: %w", keyVaultKey.Name, err) + } + for _, key := range pager.Value { + versionHistory = append(versionHistory, key.KID.Version()) + } } - publicKey, err := getKeyFromKeyBundle(keyBundle) - if err != nil { - return nil, nil, fmt.Errorf("failed to get key from key bundle:%w", err) + for _, version := range versionHistory[:keyVaultKey.VersionHistoryLimit] { + keyResponse, err := s.keyKVClient.GetKey(ctx, keyVaultKey.Name, version) + if err != nil { + return nil, nil, fmt.Errorf("failed to get key objectName:%s, objectVersion:%s, error: %w", keyVaultKey.Name, version, err) + } + keyBundle := keyResponse.KeyBundle + isEnabled := *keyBundle.Attributes.Enabled + + if !isEnabled { + lastRefresh := time.Now().Format(time.RFC3339) + keyProperties := getStatusProperty(keyVaultKey.Name, version, lastRefresh, isEnabled) + keysStatus = append(keysStatus, keyProperties) + mapKey := keymanagementprovider.KMPMapKey{Name: keyVaultKey.Name, Version: version, Enabled: isEnabled} + keymanagementprovider.DeleteKeyFromMap(s.resource, mapKey) + continue + } + + publicKey, err := getKeyFromKeyBundle(keyBundle) + if err != nil { + return nil, nil, fmt.Errorf("failed to get key from key bundle:%w", err) + } + keysMap[keymanagementprovider.KMPMapKey{Name: keyVaultKey.Name, Version: version, Enabled: isEnabled}] = publicKey + metrics.ReportAKVCertificateDuration(ctx, time.Since(startTime).Milliseconds(), keyVaultKey.Name) + keyProperties := getStatusProperty(keyVaultKey.Name, version, time.Now().Format(time.RFC3339), isEnabled) + keysStatus = append(keysStatus, keyProperties) } - keysMap[keymanagementprovider.KMPMapKey{Name: keyVaultKey.Name, Version: keyVaultKey.Version, Enabled: isEnabled}] = publicKey - metrics.ReportAKVCertificateDuration(ctx, time.Since(startTime).Milliseconds(), keyVaultKey.Name) - properties := getStatusProperty(keyVaultKey.Name, keyVaultKey.Version, time.Now().Format(time.RFC3339), isEnabled) - keysStatus = append(keysStatus, properties) } return keysMap, getStatusMap(keysStatus, types.KeysStatus), nil diff --git a/pkg/keymanagementprovider/azurekeyvault/provider_test.go b/pkg/keymanagementprovider/azurekeyvault/provider_test.go index 9bb444ae6..dffbfbd04 100644 --- a/pkg/keymanagementprovider/azurekeyvault/provider_test.go +++ b/pkg/keymanagementprovider/azurekeyvault/provider_test.go @@ -28,6 +28,7 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azcertificates" "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys" @@ -46,6 +47,22 @@ func TestCreate(t *testing.T) { config config.KeyManagementProviderConfig expectErr bool }{ + { + name: "valid config with versionHistory", + config: config.KeyManagementProviderConfig{ + "inline": "azurekeyvault", + "vaultURI": "https://testkv.vault.azure.net/", + "tenantID": "tid", + "clientID": "clientid", + "certificates": []map[string]interface{}{ + { + "name": "cert1", + "versionHistory": 2, + }, + }, + }, + expectErr: false, + }, { name: "valid config", config: config.KeyManagementProviderConfig{ @@ -170,13 +187,15 @@ func TestGetCertificates_original(t *testing.T) { } type MockKeyKVClient struct { - GetKeyFunc func(ctx context.Context, keyName string, keyVersion string) (azkeys.GetKeyResponse, error) + GetKeyFunc func(ctx context.Context, keyName string, keyVersion string) (azkeys.GetKeyResponse, error) + NewListKeyVersionsPagerFunc func(keyName string, options *azkeys.ListKeyVersionsOptions) *runtime.Pager[azkeys.ListKeyVersionsResponse] } type MockSecretKVClient struct { GetSecretFunc func(ctx context.Context, secretName string, secretVersion string) (azsecrets.GetSecretResponse, error) } type MockCertificateKVClient struct { - GetCertificateFunc func(ctx context.Context, certificateName string, certificateVersion string) (azcertificates.GetCertificateResponse, error) + GetCertificateFunc func(ctx context.Context, certificateName string, certificateVersion string) (azcertificates.GetCertificateResponse, error) + NewListCertificateVersionsPagerFunc func(certificateName string, options *azcertificates.ListCertificateVersionsOptions) *runtime.Pager[azcertificates.ListCertificateVersionsResponse] } func (m *MockKeyKVClient) GetKey(ctx context.Context, keyName string, keyVersion string) (azkeys.GetKeyResponse, error) { @@ -185,12 +204,40 @@ func (m *MockKeyKVClient) GetKey(ctx context.Context, keyName string, keyVersion } return azkeys.GetKeyResponse{}, nil } + +func (m *MockKeyKVClient) NewListKeyVersionsPager(keyName string, options *azkeys.ListKeyVersionsOptions) *runtime.Pager[azkeys.ListKeyVersionsResponse] { + if m.NewListKeyVersionsPagerFunc != nil { + return m.NewListKeyVersionsPagerFunc(keyName, options) + } + return runtime.NewPager(runtime.PagingHandler[azkeys.ListKeyVersionsResponse]{ + More: func(resp azkeys.ListKeyVersionsResponse) bool { + return false + }, + Fetcher: func(ctx context.Context, _ *azkeys.ListKeyVersionsResponse) (azkeys.ListKeyVersionsResponse, error) { + var resp azkeys.ListKeyVersionsResponse + var keyID azkeys.ID = "https://testkv.vault.azure.net/keys/key1/c1f03df1113d460491d970737dfdc35d" + resp = azkeys.ListKeyVersionsResponse{ + KeyListResult: azkeys.KeyListResult{ + NextLink: nil, + Value: []*azkeys.KeyItem{ + { + KID: &keyID, + }, + }, + }, + } + return resp, nil + }, + }) +} + func (m *MockSecretKVClient) GetSecret(ctx context.Context, secretName string, secretVersion string) (azsecrets.GetSecretResponse, error) { if m.GetSecretFunc != nil { return m.GetSecretFunc(ctx, secretName, secretVersion) } return azsecrets.GetSecretResponse{}, nil } + func (m *MockCertificateKVClient) GetCertificate(ctx context.Context, certificateName string, certificateVersion string) (azcertificates.GetCertificateResponse, error) { if m.GetCertificateFunc != nil { return m.GetCertificateFunc(ctx, certificateName, certificateVersion) @@ -198,6 +245,32 @@ func (m *MockCertificateKVClient) GetCertificate(ctx context.Context, certificat return azcertificates.GetCertificateResponse{}, nil } +func (m *MockCertificateKVClient) NewListCertificateVersionsPager(certificateName string, options *azcertificates.ListCertificateVersionsOptions) *runtime.Pager[azcertificates.ListCertificateVersionsResponse] { + if m.NewListCertificateVersionsPagerFunc != nil { + return m.NewListCertificateVersionsPagerFunc(certificateName, options) + } + return runtime.NewPager(runtime.PagingHandler[azcertificates.ListCertificateVersionsResponse]{ + More: func(resp azcertificates.ListCertificateVersionsResponse) bool { + return false + }, + Fetcher: func(ctx context.Context, _ *azcertificates.ListCertificateVersionsResponse) (azcertificates.ListCertificateVersionsResponse, error) { + var resp azcertificates.ListCertificateVersionsResponse + var certID azcertificates.ID = "https://testkv.vault.azure.net/certificates/cert1/c1f03df1113d460491d970737dfdc35d" + resp = azcertificates.ListCertificateVersionsResponse{ + CertificateListResult: azcertificates.CertificateListResult{ + NextLink: nil, + Value: []*azcertificates.CertificateItem{ + { + ID: &certID, + }, + }, + }, + } + return resp, nil + }, + }) +} + // stringPtr returns a pointer to the given string. func stringPtr(s string) *string { return &s @@ -210,26 +283,19 @@ func boolPtr(b bool) *bool { // TestGetCertificates tests the GetCertificates function func TestGetCertificates(t *testing.T) { - certID := azcertificates.ID("https://testkv.vault.azure.net/certificates/cert1") + certID := azcertificates.ID("https://testkv.vault.azure.net/certificates/cert1/d47a1c09f5b6437da28e9c72b1f4e0fd") + certIDLatest := azcertificates.ID("https://testkv.vault.azure.net/certificates/cert1/8f2e5a13c4b74960d7a8e2f1c0d6b3a9") secretID := azsecrets.ID("https://testkv.vault.azure.net/secrets/secret1") testCases := []struct { name string + versionHistoryLimit int mockKeyKVClient *MockKeyKVClient mockSecretKVClient *MockSecretKVClient mockCertificateKVClient *MockCertificateKVClient expectedErr bool }{ { - name: "GetSecret error", - mockSecretKVClient: &MockSecretKVClient{ - GetSecretFunc: func(_ context.Context, _ string, _ string) (azsecrets.GetSecretResponse, error) { - return azsecrets.GetSecretResponse{}, errors.New("error") - }, - }, - expectedErr: true, - }, - { - name: "Certificate disabled", + name: "Certificate enabled with multiple versions", mockCertificateKVClient: &MockCertificateKVClient{ GetCertificateFunc: func(_ context.Context, _ string, _ string) (azcertificates.GetCertificateResponse, error) { return azcertificates.GetCertificateResponse{ @@ -237,40 +303,94 @@ func TestGetCertificates(t *testing.T) { ID: &certID, KID: stringPtr("https://testkv.vault.azure.net/keys/key1"), Attributes: &azcertificates.CertificateAttributes{ - Enabled: boolPtr(false), + Enabled: boolPtr(true), }, }, }, nil }, + NewListCertificateVersionsPagerFunc: func(certificateName string, options *azcertificates.ListCertificateVersionsOptions) *runtime.Pager[azcertificates.ListCertificateVersionsResponse] { + pageCounter := 0 + return runtime.NewPager(runtime.PagingHandler[azcertificates.ListCertificateVersionsResponse]{ + More: func(resp azcertificates.ListCertificateVersionsResponse) bool { + return resp.CertificateListResult.NextLink != nil + }, + Fetcher: func(_ context.Context, _ *azcertificates.ListCertificateVersionsResponse) (azcertificates.ListCertificateVersionsResponse, error) { + var resp azcertificates.ListCertificateVersionsResponse + + if pageCounter == 0 { + resp = azcertificates.ListCertificateVersionsResponse{ + CertificateListResult: azcertificates.CertificateListResult{ + NextLink: stringPtr("https://testkv.vault.azure.net/certificates/cert1/versions?api-version=7.2"), + Value: []*azcertificates.CertificateItem{ + { + ID: &certID, + }, + }, + }, + } + } + + if pageCounter == 1 { + resp = azcertificates.ListCertificateVersionsResponse{ + CertificateListResult: azcertificates.CertificateListResult{ + NextLink: nil, + Value: []*azcertificates.CertificateItem{ + { + ID: &certIDLatest, + }, + }, + }, + } + } + + pageCounter++ + return resp, nil + }, + }) + }, }, mockSecretKVClient: &MockSecretKVClient{ GetSecretFunc: func(_ context.Context, _ string, _ string) (azsecrets.GetSecretResponse, error) { - rawResponse := `{ - "error": { - "code": "Forbidden", - "message": "Operation get is not allowed on a disabled secret.", - "innererror": { - "code": "SecretDisabled" - } - } - }` - - httpErr := &azcore.ResponseError{ - StatusCode: http.StatusForbidden, - RawResponse: &http.Response{ - Body: io.NopCloser(strings.NewReader(rawResponse)), + return azsecrets.GetSecretResponse{ + SecretBundle: azsecrets.SecretBundle{ + ID: &secretID, + Kid: stringPtr("https://testkv.vault.azure.net/keys/key1"), + ContentType: stringPtr("application/x-pem-file"), + Attributes: &azsecrets.SecretAttributes{ + Enabled: boolPtr(true), + }, + Value: stringPtr("-----BEGIN CERTIFICATE-----\nMIIC8TCCAdmgAwIBAgIUaNrwbhs/I1ecqUYdzD2xuAVNdmowDQYJKoZIhvcNAQEL\nBQAwKjEPMA0GA1UECgwGUmF0aWZ5MRcwFQYDVQQDDA5SYXRpZnkgUm9vdCBDQTAe\nFw0yMzA2MjEwMTIyMzdaFw0yNDA2MjAwMTIyMzdaMBkxFzAVBgNVBAMMDnJhdGlm\neS5kZWZhdWx0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtskG1BUt\n4Fw2lbm53KbwZb1hnLmWdwRotZyznhhk/yrUDcq3uF6klwpk/E2IKfUKIo6doHSk\nXaEZXR68UtXygvA4wdg7xZ6kKpXy0gu+RxGE6CGtDHTyDDzITu+NBjo21ZSsyGpQ\nJeIKftUCHdwdygKf0CdJx8A29GBRpHGCmJadmt7tTzOnYjmbuPVLeqJo/Ex9qXcG\nZbxoxnxr5NCocFeKx+EbLo+k/KjdFB2PKnhgzxAaMMMP6eXPr8l5AlzkC83EmPvN\ntveuaBbamdlFkD+53TZeZlxt3GIdq93Iw/UpbQ/pvhbrztMT+UVEkm15sShfX8Xn\nL2st5A4n0V+66QIDAQABoyAwHjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIH\ngDANBgkqhkiG9w0BAQsFAAOCAQEAGpOqozyfDSBjoTepsRroxxcZ4sq65gw45Bme\nm36BS6FG0WHIg3cMy6KIIBefTDSKrPkKNTtuF25AeGn9jM+26cnfDM78ZH0+Lnn7\n7hs0MA64WMPQaWs9/+89aM9NADV9vp2zdG4xMi6B7DruvKWyhJaNoRqK/qP6LdSQ\nw8M+21sAHvXgrRkQtJlVOzVhgwt36NOb1hzRlQiZB+nhv2Wbw7fbtAaADk3JAumf\nvM+YdPS1KfAFaYefm4yFd+9/C0KOkHico3LTbELO5hG0Mo/EYvtjM+Fljb42EweF\n3nAx1GSPe5Tn8p3h6RyJW5HIKozEKyfDuLS0ccB/nqT3oNjcTw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDRTCCAi2gAwIBAgIUcC33VfaMhOnsl7avNTRVQozoVtUwDQYJKoZIhvcNAQEL\nBQAwKjEPMA0GA1UECgwGUmF0aWZ5MRcwFQYDVQQDDA5SYXRpZnkgUm9vdCBDQTAe\nFw0yMzA2MjEwMTIyMzZaFw0yMzA2MjIwMTIyMzZaMCoxDzANBgNVBAoMBlJhdGlm\neTEXMBUGA1UEAwwOUmF0aWZ5IFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB\nDwAwggEKAoIBAQDDFhDnyPrVDZaeRu6Tbg1a/iTwus+IuX+h8aKhKS1yHz4EF/Lz\nxCy7lNSQ9srGMMVumWuNom/ydIphff6PejZM1jFKPU6OQR/0JX5epcVIjbKa562T\nDguUxJ+h5V3EIyM4RqOWQ2g/xZo86x5TzyNJXiVdHHRvmDvUNwPpMeDjr/EHVAni\n5YQObxkJRiiZ7XOa5zz3YztVm8sSZAwPWroY1HIfvtP+KHpiNDIKSymmuJkH4SEr\nJn++iqN8na18a9DFBPTTrLPe3CxATGrMfosCMZ6LP3iFLLc/FaSpwcnugWdewsUK\nYs+sUY7jFWR7x7/1nyFWyRrQviM4f4TY+K7NAgMBAAGjYzBhMB0GA1UdDgQWBBQH\nYePW7QPP2p1utr3r6gqzEkKs+DAfBgNVHSMEGDAWgBQHYePW7QPP2p1utr3r6gqz\nEkKs+DAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwICBDANBgkqhkiG9w0B\nAQsFAAOCAQEAjKp4vx3bFaKVhAbQeTsDjWJgmXLK2vLgt74MiUwSF6t0wehlfszE\nIcJagGJsvs5wKFf91bnwiqwPjmpse/thPNBAxh1uEoh81tOklv0BN790vsVpq3t+\ncnUvWPiCZdRlAiGGFtRmKk3Keq4sM6UdiUki9s+wnxypHVb4wIpVxu5R271Lnp5I\n+rb2EQ48iblt4XZPczf/5QJdTgbItjBNbuO8WVPOqUIhCiFuAQziLtNUq3p81dHO\nQ2BPgmaitCpIUYHVYighLauBGCH8xOFzj4a4KbOxKdxyJTd0La/vRCKaUtJX67Lc\nfQYVR9HXQZ0YlmwPcmIG5v7wBfcW34NUvA==\n-----END CERTIFICATE-----\n"), }, - } - return azsecrets.GetSecretResponse{}, httpErr + }, nil }, }, expectedErr: false, }, { - name: "Certificate disabled error", + name: "GetSecret error", + versionHistoryLimit: 1, + mockCertificateKVClient: &MockCertificateKVClient{}, + mockSecretKVClient: &MockSecretKVClient{ + GetSecretFunc: func(_ context.Context, _ string, _ string) (azsecrets.GetSecretResponse, error) { + return azsecrets.GetSecretResponse{}, errors.New("error") + }, + }, + expectedErr: true, + }, + { + name: "Certificate secret disabled", + versionHistoryLimit: 1, mockCertificateKVClient: &MockCertificateKVClient{ GetCertificateFunc: func(_ context.Context, _ string, _ string) (azcertificates.GetCertificateResponse, error) { - return azcertificates.GetCertificateResponse{}, errors.New("error") + return azcertificates.GetCertificateResponse{ + CertificateBundle: azcertificates.CertificateBundle{ + ID: &certID, + KID: stringPtr("https://testkv.vault.azure.net/keys/key1"), + Attributes: &azcertificates.CertificateAttributes{ + Enabled: boolPtr(false), + }, + }, + }, nil }, }, mockSecretKVClient: &MockSecretKVClient{ @@ -294,42 +414,75 @@ func TestGetCertificates(t *testing.T) { return azsecrets.GetSecretResponse{}, httpErr }, }, - expectedErr: true, - }, - { - name: "Certificate enabled", - mockCertificateKVClient: &MockCertificateKVClient{ - GetCertificateFunc: func(_ context.Context, _ string, _ string) (azcertificates.GetCertificateResponse, error) { - return azcertificates.GetCertificateResponse{ - CertificateBundle: azcertificates.CertificateBundle{ - ID: &certID, - KID: stringPtr("https://testkv.vault.azure.net/keys/key1"), - Attributes: &azcertificates.CertificateAttributes{ - Enabled: boolPtr(true), - }, - }, - }, nil - }, - }, - mockSecretKVClient: &MockSecretKVClient{ - GetSecretFunc: func(_ context.Context, _ string, _ string) (azsecrets.GetSecretResponse, error) { - return azsecrets.GetSecretResponse{ - SecretBundle: azsecrets.SecretBundle{ - ID: &secretID, - Kid: stringPtr("https://testkv.vault.azure.net/keys/key1"), - ContentType: stringPtr("application/x-pem-file"), - Attributes: &azsecrets.SecretAttributes{ - Enabled: boolPtr(true), - }, - Value: stringPtr("-----BEGIN CERTIFICATE-----\nMIIC8TCCAdmgAwIBAgIUaNrwbhs/I1ecqUYdzD2xuAVNdmowDQYJKoZIhvcNAQEL\nBQAwKjEPMA0GA1UECgwGUmF0aWZ5MRcwFQYDVQQDDA5SYXRpZnkgUm9vdCBDQTAe\nFw0yMzA2MjEwMTIyMzdaFw0yNDA2MjAwMTIyMzdaMBkxFzAVBgNVBAMMDnJhdGlm\neS5kZWZhdWx0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtskG1BUt\n4Fw2lbm53KbwZb1hnLmWdwRotZyznhhk/yrUDcq3uF6klwpk/E2IKfUKIo6doHSk\nXaEZXR68UtXygvA4wdg7xZ6kKpXy0gu+RxGE6CGtDHTyDDzITu+NBjo21ZSsyGpQ\nJeIKftUCHdwdygKf0CdJx8A29GBRpHGCmJadmt7tTzOnYjmbuPVLeqJo/Ex9qXcG\nZbxoxnxr5NCocFeKx+EbLo+k/KjdFB2PKnhgzxAaMMMP6eXPr8l5AlzkC83EmPvN\ntveuaBbamdlFkD+53TZeZlxt3GIdq93Iw/UpbQ/pvhbrztMT+UVEkm15sShfX8Xn\nL2st5A4n0V+66QIDAQABoyAwHjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIH\ngDANBgkqhkiG9w0BAQsFAAOCAQEAGpOqozyfDSBjoTepsRroxxcZ4sq65gw45Bme\nm36BS6FG0WHIg3cMy6KIIBefTDSKrPkKNTtuF25AeGn9jM+26cnfDM78ZH0+Lnn7\n7hs0MA64WMPQaWs9/+89aM9NADV9vp2zdG4xMi6B7DruvKWyhJaNoRqK/qP6LdSQ\nw8M+21sAHvXgrRkQtJlVOzVhgwt36NOb1hzRlQiZB+nhv2Wbw7fbtAaADk3JAumf\nvM+YdPS1KfAFaYefm4yFd+9/C0KOkHico3LTbELO5hG0Mo/EYvtjM+Fljb42EweF\n3nAx1GSPe5Tn8p3h6RyJW5HIKozEKyfDuLS0ccB/nqT3oNjcTw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDRTCCAi2gAwIBAgIUcC33VfaMhOnsl7avNTRVQozoVtUwDQYJKoZIhvcNAQEL\nBQAwKjEPMA0GA1UECgwGUmF0aWZ5MRcwFQYDVQQDDA5SYXRpZnkgUm9vdCBDQTAe\nFw0yMzA2MjEwMTIyMzZaFw0yMzA2MjIwMTIyMzZaMCoxDzANBgNVBAoMBlJhdGlm\neTEXMBUGA1UEAwwOUmF0aWZ5IFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB\nDwAwggEKAoIBAQDDFhDnyPrVDZaeRu6Tbg1a/iTwus+IuX+h8aKhKS1yHz4EF/Lz\nxCy7lNSQ9srGMMVumWuNom/ydIphff6PejZM1jFKPU6OQR/0JX5epcVIjbKa562T\nDguUxJ+h5V3EIyM4RqOWQ2g/xZo86x5TzyNJXiVdHHRvmDvUNwPpMeDjr/EHVAni\n5YQObxkJRiiZ7XOa5zz3YztVm8sSZAwPWroY1HIfvtP+KHpiNDIKSymmuJkH4SEr\nJn++iqN8na18a9DFBPTTrLPe3CxATGrMfosCMZ6LP3iFLLc/FaSpwcnugWdewsUK\nYs+sUY7jFWR7x7/1nyFWyRrQviM4f4TY+K7NAgMBAAGjYzBhMB0GA1UdDgQWBBQH\nYePW7QPP2p1utr3r6gqzEkKs+DAfBgNVHSMEGDAWgBQHYePW7QPP2p1utr3r6gqz\nEkKs+DAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwICBDANBgkqhkiG9w0B\nAQsFAAOCAQEAjKp4vx3bFaKVhAbQeTsDjWJgmXLK2vLgt74MiUwSF6t0wehlfszE\nIcJagGJsvs5wKFf91bnwiqwPjmpse/thPNBAxh1uEoh81tOklv0BN790vsVpq3t+\ncnUvWPiCZdRlAiGGFtRmKk3Keq4sM6UdiUki9s+wnxypHVb4wIpVxu5R271Lnp5I\n+rb2EQ48iblt4XZPczf/5QJdTgbItjBNbuO8WVPOqUIhCiFuAQziLtNUq3p81dHO\nQ2BPgmaitCpIUYHVYighLauBGCH8xOFzj4a4KbOxKdxyJTd0La/vRCKaUtJX67Lc\nfQYVR9HXQZ0YlmwPcmIG5v7wBfcW34NUvA==\n-----END CERTIFICATE-----\n"), - }, - }, nil - }, - }, expectedErr: false, }, + // { + // name: "Certificate disabled error", + // versionHistoryLimit: 1, + // mockCertificateKVClient: &MockCertificateKVClient{ + // GetCertificateFunc: func(_ context.Context, _ string, _ string) (azcertificates.GetCertificateResponse, error) { + // return azcertificates.GetCertificateResponse{}, errors.New("error") + // }, + // }, + // mockSecretKVClient: &MockSecretKVClient{ + // GetSecretFunc: func(_ context.Context, _ string, _ string) (azsecrets.GetSecretResponse, error) { + // rawResponse := `{ + // "error": { + // "code": "Forbidden", + // "message": "Operation get is not allowed on a disabled secret.", + // "innererror": { + // "code": "SecretDisabled" + // } + // } + // }` + + // httpErr := &azcore.ResponseError{ + // StatusCode: http.StatusForbidden, + // RawResponse: &http.Response{ + // Body: io.NopCloser(strings.NewReader(rawResponse)), + // }, + // } + // return azsecrets.GetSecretResponse{}, httpErr + // }, + // }, + // expectedErr: true, + // }, + // { + // name: "Certificate enabled", + // mockCertificateKVClient: &MockCertificateKVClient{ + // GetCertificateFunc: func(_ context.Context, _ string, _ string) (azcertificates.GetCertificateResponse, error) { + // return azcertificates.GetCertificateResponse{ + // CertificateBundle: azcertificates.CertificateBundle{ + // ID: &certID, + // KID: stringPtr("https://testkv.vault.azure.net/keys/key1"), + // Attributes: &azcertificates.CertificateAttributes{ + // Enabled: boolPtr(true), + // }, + // }, + // }, nil + // }, + // }, + // mockSecretKVClient: &MockSecretKVClient{ + // GetSecretFunc: func(_ context.Context, _ string, _ string) (azsecrets.GetSecretResponse, error) { + // return azsecrets.GetSecretResponse{ + // SecretBundle: azsecrets.SecretBundle{ + // ID: &secretID, + // Kid: stringPtr("https://testkv.vault.azure.net/keys/key1"), + // ContentType: stringPtr("application/x-pem-file"), + // Attributes: &azsecrets.SecretAttributes{ + // Enabled: boolPtr(true), + // }, + // Value: stringPtr("-----BEGIN CERTIFICATE-----\nMIIC8TCCAdmgAwIBAgIUaNrwbhs/I1ecqUYdzD2xuAVNdmowDQYJKoZIhvcNAQEL\nBQAwKjEPMA0GA1UECgwGUmF0aWZ5MRcwFQYDVQQDDA5SYXRpZnkgUm9vdCBDQTAe\nFw0yMzA2MjEwMTIyMzdaFw0yNDA2MjAwMTIyMzdaMBkxFzAVBgNVBAMMDnJhdGlm\neS5kZWZhdWx0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtskG1BUt\n4Fw2lbm53KbwZb1hnLmWdwRotZyznhhk/yrUDcq3uF6klwpk/E2IKfUKIo6doHSk\nXaEZXR68UtXygvA4wdg7xZ6kKpXy0gu+RxGE6CGtDHTyDDzITu+NBjo21ZSsyGpQ\nJeIKftUCHdwdygKf0CdJx8A29GBRpHGCmJadmt7tTzOnYjmbuPVLeqJo/Ex9qXcG\nZbxoxnxr5NCocFeKx+EbLo+k/KjdFB2PKnhgzxAaMMMP6eXPr8l5AlzkC83EmPvN\ntveuaBbamdlFkD+53TZeZlxt3GIdq93Iw/UpbQ/pvhbrztMT+UVEkm15sShfX8Xn\nL2st5A4n0V+66QIDAQABoyAwHjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIH\ngDANBgkqhkiG9w0BAQsFAAOCAQEAGpOqozyfDSBjoTepsRroxxcZ4sq65gw45Bme\nm36BS6FG0WHIg3cMy6KIIBefTDSKrPkKNTtuF25AeGn9jM+26cnfDM78ZH0+Lnn7\n7hs0MA64WMPQaWs9/+89aM9NADV9vp2zdG4xMi6B7DruvKWyhJaNoRqK/qP6LdSQ\nw8M+21sAHvXgrRkQtJlVOzVhgwt36NOb1hzRlQiZB+nhv2Wbw7fbtAaADk3JAumf\nvM+YdPS1KfAFaYefm4yFd+9/C0KOkHico3LTbELO5hG0Mo/EYvtjM+Fljb42EweF\n3nAx1GSPe5Tn8p3h6RyJW5HIKozEKyfDuLS0ccB/nqT3oNjcTw==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDRTCCAi2gAwIBAgIUcC33VfaMhOnsl7avNTRVQozoVtUwDQYJKoZIhvcNAQEL\nBQAwKjEPMA0GA1UECgwGUmF0aWZ5MRcwFQYDVQQDDA5SYXRpZnkgUm9vdCBDQTAe\nFw0yMzA2MjEwMTIyMzZaFw0yMzA2MjIwMTIyMzZaMCoxDzANBgNVBAoMBlJhdGlm\neTEXMBUGA1UEAwwOUmF0aWZ5IFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB\nDwAwggEKAoIBAQDDFhDnyPrVDZaeRu6Tbg1a/iTwus+IuX+h8aKhKS1yHz4EF/Lz\nxCy7lNSQ9srGMMVumWuNom/ydIphff6PejZM1jFKPU6OQR/0JX5epcVIjbKa562T\nDguUxJ+h5V3EIyM4RqOWQ2g/xZo86x5TzyNJXiVdHHRvmDvUNwPpMeDjr/EHVAni\n5YQObxkJRiiZ7XOa5zz3YztVm8sSZAwPWroY1HIfvtP+KHpiNDIKSymmuJkH4SEr\nJn++iqN8na18a9DFBPTTrLPe3CxATGrMfosCMZ6LP3iFLLc/FaSpwcnugWdewsUK\nYs+sUY7jFWR7x7/1nyFWyRrQviM4f4TY+K7NAgMBAAGjYzBhMB0GA1UdDgQWBBQH\nYePW7QPP2p1utr3r6gqzEkKs+DAfBgNVHSMEGDAWgBQHYePW7QPP2p1utr3r6gqz\nEkKs+DAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwICBDANBgkqhkiG9w0B\nAQsFAAOCAQEAjKp4vx3bFaKVhAbQeTsDjWJgmXLK2vLgt74MiUwSF6t0wehlfszE\nIcJagGJsvs5wKFf91bnwiqwPjmpse/thPNBAxh1uEoh81tOklv0BN790vsVpq3t+\ncnUvWPiCZdRlAiGGFtRmKk3Keq4sM6UdiUki9s+wnxypHVb4wIpVxu5R271Lnp5I\n+rb2EQ48iblt4XZPczf/5QJdTgbItjBNbuO8WVPOqUIhCiFuAQziLtNUq3p81dHO\nQ2BPgmaitCpIUYHVYighLauBGCH8xOFzj4a4KbOxKdxyJTd0La/vRCKaUtJX67Lc\nfQYVR9HXQZ0YlmwPcmIG5v7wBfcW34NUvA==\n-----END CERTIFICATE-----\n"), + // }, + // }, nil + // }, + // }, + // expectedErr: false, + // }, { - name: "getCertsFromSecretBundle error", + name: "getCertsFromSecretBundle error", + versionHistoryLimit: 1, + mockCertificateKVClient: &MockCertificateKVClient{}, mockSecretKVClient: &MockSecretKVClient{ GetSecretFunc: func(_ context.Context, _ string, _ string) (azsecrets.GetSecretResponse, error) { return azsecrets.GetSecretResponse{ @@ -354,8 +507,9 @@ func TestGetCertificates(t *testing.T) { provider := &akvKMProvider{ certificates: []types.KeyVaultValue{ { - Name: "cert1", - Version: "c1f03df1113d460491d970737dfdc35d", + Name: "cert1", + Version: "c1f03df1113d460491d970737dfdc35d", + VersionHistoryLimit: tc.versionHistoryLimit, }, }, keyKVClient: tc.mockKeyKVClient, @@ -373,13 +527,75 @@ func TestGetCertificates(t *testing.T) { // TestGetKeys tests the GetKeys function func TestGetKeys(t *testing.T) { - keyID := azkeys.ID("https://testkv.vault.azure.net/keys/key1") + keyID := azkeys.ID("https://testkv.vault.azure.net/keys/key1/c1f03df1113d460491d970737dfdc35d") + keyIDLatest := azkeys.ID("https://testkv.vault.azure.net/keys/key1/8f2e5a13c4b74960d7a8e2f1c0d6b3a9") keyTY := azkeys.JSONWebKeyTypeRSA testCases := []struct { - name string - mockKeyKVClient *MockKeyKVClient - expectedErr bool + name string + versionHistoryLimit int + mockKeyKVClient *MockKeyKVClient + expectedErr bool }{ + { + name: "Key enabled with multiple versions", + versionHistoryLimit: 2, + mockKeyKVClient: &MockKeyKVClient{ + GetKeyFunc: func(_ context.Context, _ string, _ string) (azkeys.GetKeyResponse, error) { + return azkeys.GetKeyResponse{ + KeyBundle: azkeys.KeyBundle{ + Key: &azkeys.JSONWebKey{ + KID: &keyID, + Kty: &keyTY, + N: []byte("n"), + E: []byte("e"), + }, + Attributes: &azkeys.KeyAttributes{ + Enabled: boolPtr(true), + }, + }, + }, nil + }, + NewListKeyVersionsPagerFunc: func(_ string, _ *azkeys.ListKeyVersionsOptions) *runtime.Pager[azkeys.ListKeyVersionsResponse] { + pageCounter := 0 + return runtime.NewPager(runtime.PagingHandler[azkeys.ListKeyVersionsResponse]{ + More: func(resp azkeys.ListKeyVersionsResponse) bool { + return resp.KeyListResult.NextLink != nil + }, + Fetcher: func(_ context.Context, _ *azkeys.ListKeyVersionsResponse) (azkeys.ListKeyVersionsResponse, error) { + var resp azkeys.ListKeyVersionsResponse + if pageCounter == 0 { + resp = azkeys.ListKeyVersionsResponse{ + KeyListResult: azkeys.KeyListResult{ + NextLink: stringPtr("https://testkv.vault.azure.net/keys/key1/versions?api-version=7.2"), + Value: []*azkeys.KeyItem{ + { + KID: &keyID, + }, + }, + }, + } + } + + if pageCounter == 1 { + resp = azkeys.ListKeyVersionsResponse{ + KeyListResult: azkeys.KeyListResult{ + Value: []*azkeys.KeyItem{ + { + KID: &keyIDLatest, + }, + }, + }, + } + } + + pageCounter++ + return resp, nil + }, + }) + }, + }, + expectedErr: false, + }, { name: "GetKey error", mockKeyKVClient: &MockKeyKVClient{ @@ -453,8 +669,9 @@ func TestGetKeys(t *testing.T) { provider := &akvKMProvider{ keys: []types.KeyVaultValue{ { - Name: "key1", - Version: "c1f03df1113d460491d970737dfdc35d", + Name: "key1", + Version: "c1f03df1113d460491d970737dfdc35d", + VersionHistoryLimit: tc.versionHistoryLimit, }, }, keyKVClient: tc.mockKeyKVClient, diff --git a/pkg/keymanagementprovider/azurekeyvault/types/types.go b/pkg/keymanagementprovider/azurekeyvault/types/types.go index e51650dab..3acf572df 100644 --- a/pkg/keymanagementprovider/azurekeyvault/types/types.go +++ b/pkg/keymanagementprovider/azurekeyvault/types/types.go @@ -37,4 +37,6 @@ type KeyVaultValue struct { Name string `json:"name" yaml:"name"` // the version of the Azure Key Vault certificate/key Version string `json:"version" yaml:"version"` + // the number of versions to keep in the cache + VersionHistoryLimit int `json:"versionHistoryLimit" yaml:"versionHistoryLimit"` }