From 4a2fca9f4cdc3eeb2bcf96339105b09e862c16b1 Mon Sep 17 00:00:00 2001 From: Yves Galante Date: Thu, 31 Aug 2023 16:55:03 +0200 Subject: [PATCH] feat(azure): Support Azure Workload Identity #421 Signed-off-by: Yves Galante --- go.mod | 11 +- go.sum | 5 + pkg/backends/azurekeyvault.go | 96 +++--- pkg/backends/azurekeyvault_test.go | 462 +++++++++++++---------------- pkg/config/config.go | 10 +- pkg/config/config_test.go | 8 - 6 files changed, 287 insertions(+), 305 deletions(-) diff --git a/go.mod b/go.mod index 057b5432..d06f324c 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,9 @@ go 1.21 require ( cloud.google.com/go/secretmanager v1.11.2 github.com/1Password/connect-sdk-go v1.5.3 - github.com/Azure/azure-sdk-for-go v68.0.0+incompatible + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1 + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.0.1 github.com/DelineaXPM/tss-sdk-go/v2 v2.0.0 github.com/IBM/go-sdk-core/v5 v5.14.1 github.com/IBM/secrets-manager-go-sdk v1.2.0 @@ -41,6 +43,9 @@ require ( cloud.google.com/go/kms v1.15.2 // indirect cloud.google.com/go/monitoring v1.16.0 // indirect filippo.io/age v1.0.0 // indirect + github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.29 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.23 // indirect @@ -51,6 +56,7 @@ require ( github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect github.com/BurntSushi/toml v1.3.2 // indirect github.com/DataDog/datadog-go v3.2.0+incompatible // indirect github.com/Jeffail/gabs v1.1.1 // indirect @@ -124,6 +130,7 @@ require ( github.com/go-test/deep v1.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang-jwt/jwt/v5 v5.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect @@ -204,6 +211,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/kelseyhightower/envconfig v1.4.0 // indirect github.com/klauspost/compress v1.17.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/leodido/go-urn v1.2.3 // indirect github.com/lib/pq v1.10.9 // indirect github.com/linode/linodego v0.7.1 // indirect @@ -239,6 +247,7 @@ require ( github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pires/go-proxyproto v0.6.1 // indirect + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/posener/complete v1.2.3 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect diff --git a/go.sum b/go.sum index 4d1ad331..b03a5fcc 100644 --- a/go.sum +++ b/go.sum @@ -76,6 +76,10 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.1.0 h1:Q707j github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.1.0/go.mod h1:vjoxsjVnPwhjHZw4PuuhpgYlcxWl5tyNedLHUl0ulFA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1 h1:7CBQ+Ei8SP2c6ydQTGCCrS35bDxgTMfoP2miAwK++OU= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1/go.mod h1:c/wcGeGx5FUPbM/JltUYHZcKmigwyVLJlDq+4HdtXaw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.0.1 h1:8TkzQBrN9PWIwo7ekdd696KpC6IfTltV2/F8qKKBWik= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.0.1/go.mod h1:aprFpXPQiTyG5Rkz6Ot5pvU6y6YKg/AKYOcLCoxN0bk= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0 h1:u/LLAOFgsMv7HmNL4Qufg58y+qElGOt5qv0z1mURkRY= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0/go.mod h1:2e8rMJtl2+2j+HXbTBwnyGpm5Nou7KhvSfxOq8JpTag= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= @@ -1814,6 +1818,7 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/pkg/backends/azurekeyvault.go b/pkg/backends/azurekeyvault.go index be88305a..ec68ad29 100644 --- a/pkg/backends/azurekeyvault.go +++ b/pkg/backends/azurekeyvault.go @@ -3,22 +3,31 @@ package backends import ( "context" "fmt" - "github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault" + "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/security/keyvault/azsecrets" "github.com/argoproj-labs/argocd-vault-plugin/pkg/utils" - "path" - "strings" "time" ) // AzureKeyVault is a struct for working with an Azure Key Vault backend type AzureKeyVault struct { - Client keyvault.BaseClient + Credential azcore.TokenCredential + ClientBuilder func(vaultURL string, credential azcore.TokenCredential, options *azsecrets.ClientOptions) (AzSecretsClient, error) +} + +type AzSecretsClient interface { + GetSecret(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) + NewListSecretPropertiesPager(options *azsecrets.ListSecretPropertiesOptions) *runtime.Pager[azsecrets.ListSecretPropertiesResponse] } // NewAzureKeyVaultBackend initializes a new Azure Key Vault backend -func NewAzureKeyVaultBackend(client keyvault.BaseClient) *AzureKeyVault { +func NewAzureKeyVaultBackend(credential azcore.TokenCredential, clientBuilder func(vaultURL string, credential azcore.TokenCredential, options *azsecrets.ClientOptions) (*azsecrets.Client, error)) *AzureKeyVault { return &AzureKeyVault{ - Client: client, + Credential: credential, + ClientBuilder: func(vaultURL string, credential azcore.TokenCredential, options *azsecrets.ClientOptions) (AzSecretsClient, error) { + return clientBuilder(vaultURL, credential, options) + }, } } @@ -29,59 +38,55 @@ func (a *AzureKeyVault) Login() error { // GetSecrets gets secrets from Azure Key Vault and returns the formatted data // For Azure Key Vault, `kvpath` is the unique name of your vault +// For Azure use the version here not make really sens as each secret have a different version but let support it func (a *AzureKeyVault) GetSecrets(kvpath string, version string, _ map[string]string) (map[string]interface{}, error) { kvpath = fmt.Sprintf("https://%s.vault.azure.net", kvpath) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - data := make(map[string]interface{}) + verboseOptionalVersion("Azure Key Vault list all secrets from vault %s", version, kvpath) - utils.VerboseToStdErr("Azure Key Vault listing secrets in vault %v", kvpath) - secretList, err := a.Client.GetSecretsComplete(ctx, kvpath, nil) + client, err := a.ClientBuilder(kvpath, a.Credential, nil) if err != nil { return nil, err } - utils.VerboseToStdErr("Azure Key Vault list secrets response %v", secretList) - // Gather all secrets in Key Vault - - for ; secretList.NotDone(); secretList.NextWithContext(ctx) { - secret := path.Base(*secretList.Value().ID) - if version == "" { - utils.VerboseToStdErr("Azure Key Vault getting secret %s from vault %s", secret, kvpath) - secretResp, err := a.Client.GetSecret(ctx, kvpath, secret, "") - if err != nil { - return nil, err - } + data := make(map[string]interface{}) - utils.VerboseToStdErr("Azure Key Vault get unversioned secret response %v", secretResp) - data[secret] = *secretResp.Value - continue + pager := client.NewListSecretPropertiesPager(nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, err } - // In Azure Key Vault the versions of a secret is first shown after running GetSecretVersions. So we need - // to loop through the versions for each secret in order to find the secret that has the specific version. - secretVersions, _ := a.Client.GetSecretVersionsComplete(ctx, kvpath, secret, nil) - for ; secretVersions.NotDone(); secretVersions.NextWithContext(ctx) { - secretVersion := secretVersions.Value() + for _, secretVersion := range page.Value { // Azure Key Vault has ability to enable/disable a secret, so lets honour that if !*secretVersion.Attributes.Enabled { continue } - // Secret version matched given version - if strings.Contains(*secretVersion.ID, version) { - utils.VerboseToStdErr("Azure Key Vault getting secret %s from vault %s at version %s", secret, kvpath, version) - secretResp, err := a.Client.GetSecret(ctx, kvpath, secret, version) + name := secretVersion.ID.Name() + // Secret version matched given version ? + if version == "" || secretVersion.ID.Version() == version { + verboseOptionalVersion("Azure Key Vault getting secret %s from vault %s", version, name, kvpath) + secret, err := client.GetSecret(ctx, name, version, nil) if err != nil { return nil, err } - - utils.VerboseToStdErr("Azure Key Vault get versioned secret response %v", secretResp) - data[secret] = *secretResp.Value + utils.VerboseToStdErr("Azure Key Vault get secret response %v", secret) + data[name] = *secret.Value + } else { + verboseOptionalVersion("Azure Key Vault getting secret %s from vault %s", version, name, kvpath) + secret, err := client.GetSecret(ctx, name, version, nil) + if err != nil || !*secretVersion.Attributes.Enabled { + utils.VerboseToStdErr("Azure Key Vault get versioned secret not found %s", err) + continue + } + utils.VerboseToStdErr("Azure Key Vault get versioned secret response %v", secret) + data[name] = *secret.Value } } } - return data, nil } @@ -92,15 +97,28 @@ func (a *AzureKeyVault) GetIndividualSecret(kvpath, secret, version string, anno ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - utils.VerboseToStdErr("Azure Key Vault getting secret %s from vault %s at version %s", secret, kvpath, version) + verboseOptionalVersion("Azure Key Vault getting individual secret %s from vault %s", version, secret, kvpath) kvpath = fmt.Sprintf("https://%s.vault.azure.net", kvpath) - data, err := a.Client.GetSecret(ctx, kvpath, secret, version) + client, err := a.ClientBuilder(kvpath, a.Credential, nil) + if err != nil { + return nil, err + } + + data, err := client.GetSecret(ctx, secret, version, nil) if err != nil { return nil, err } - utils.VerboseToStdErr("Azure Key Vault get versioned secret response %v", data) + utils.VerboseToStdErr("Azure Key Vault get individual secret response %v", data) return *data.Value, nil } + +func verboseOptionalVersion(format string, version string, message ...interface{}) { + if version == "" { + utils.VerboseToStdErr(format, message...) + } else { + utils.VerboseToStdErr(format+" at version %s", append(message, version)...) + } +} diff --git a/pkg/backends/azurekeyvault_test.go b/pkg/backends/azurekeyvault_test.go index 30e8da3a..b3b0fcf2 100644 --- a/pkg/backends/azurekeyvault_test.go +++ b/pkg/backends/azurekeyvault_test.go @@ -1,286 +1,246 @@ package backends_test import ( - "fmt" - "github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault" + "context" + "errors" + "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/security/keyvault/azsecrets" "github.com/argoproj-labs/argocd-vault-plugin/pkg/backends" - "io" - "net/http" - "net/url" "reflect" - "strings" "testing" ) -type mockSender struct { - DoFunc func(r *http.Request) (*http.Response, error) -} +const secretNamePrefix = "https://myvaultname.vault.azure.net/keys/" -func (m mockSender) Do(r *http.Request) (*http.Response, error) { - return m.DoFunc(r) +type mockClientProxy struct { + simulateError string } -func TestAzureKeyVault_GetSecrets(t *testing.T) { - // secrets: list of key vault secrets (where foo and bar is present) - // foo: is a secret with a secret value - // bar: is a secret with a secret value - tt := map[string]struct { - Body string - StatusCode int - }{ - "secrets": { - Body: ` - { - "value": [ - { - "contentType": "foobar", - "id": "https://test.vault.azure.net/secrets/foo", - "attributes": { - "enabled": true, - "created": 1629833926, - "updated": 1629833926, - "recoveryLevel": "Recoverable+Purgeable" - }, - "tags": {} - }, - { - "id": "https://test.vault.azure.net/secrets/bar", - "attributes": { - "enabled": true, - "created": 1629813653, - "updated": 1629813653, - "recoveryLevel": "Recoverable+Purgeable" - }, - "tags": { - "file-encoding": "utf-8" - } - } - ], - "nextLink": null - }`, - StatusCode: 200, - }, - "foo": { - Body: ` - { - "value": "bar", - "contentType": "foobar", - "id": "https://test.vault.azure.net.test/secrets/foo/8f8da2e06c8240808ee439ff093803b5", - "attributes": { - "enabled": true, - "created": 1629833926, - "updated": 1629833926, - "recoveryLevel": "Recoverable+Purgeable" - }, - "tags": {} - }`, - StatusCode: 200, - }, - "bar": { - Body: ` - { - "value": "baz", - "id": "https://test.vault.azure.net.test/secrets/bar/33740fc26214497f8904d93f20f7db6d", - "attributes": { - "enabled": true, - "created": 1629813653, - "updated": 1629813653, - "recoveryLevel": "Recoverable+Purgeable" - }, - "tags": { - "file-encoding": "utf-8" - } - }`, - StatusCode: 200, - }, - "bar_version": { - Body: ` - { - "value": "baz-version", - "id": "https://test.vault.azure.net.test/secrets/bar/33740fc26214497f8904d93f20f7db6c", - "attributes": { - "enabled": true, - "created": 1629813653, - "updated": 1629813653, - "recoveryLevel": "Recoverable+Purgeable" - }, - "tags": { - "file-encoding": "utf-8" - } - }`, - StatusCode: 200, - }, - "bar_disabled": { - Body: ` - { - "value": "baz-disabled", - "id": "https://test.vault.azure.net.test/secrets/bar/33740fc26214497f8904d93f20f7db6b", - "attributes": { - "enabled": false, - "created": 1629813653, - "updated": 1629813653, - "recoveryLevel": "Recoverable+Purgeable" - }, - "tags": { - "file-encoding": "utf-8" - } - }`, - StatusCode: 200, - }, - "foobar": { - Body: ` - { - "value": [ - { - "value": "bar", - "id": "https://test.vault.azure.net.test/secrets/bar/33740fc26214497f8904d93f20f7db6d", - "attributes": { - "enabled": true, - "created": 1629813653, - "updated": 1629813653, - "recoveryLevel": "Recoverable+Purgeable" - }, - "tags": { - "file-encoding": "utf-8" - } - }, - { - "value": "bar", - "id": "https://test.vault.azure.net.test/secrets/bar/33740fc26214497f8904d93f20f7db6c", - "attributes": { - "enabled": true, - "created": 1629813653, - "updated": 1629813653, - "recoveryLevel": "Recoverable+Purgeable" - }, - "tags": { - "file-encoding": "utf-8" - } - }, - { - "value": "bar", - "id": "https://test.vault.azure.net.test/secrets/bar/33740fc26214497f8904d93f20f7db6b", - "attributes": { - "enabled": false, - "created": 1629813653, - "updated": 1629813653, - "recoveryLevel": "Recoverable+Purgeable" - }, - "tags": { - "file-encoding": "utf-8" - } - } - ], - "nextLink": null - }`, - StatusCode: 200, +func makeSecretProperties(id azsecrets.ID, enable bool) *azsecrets.SecretProperties { + return &azsecrets.SecretProperties{ + ID: &id, + Attributes: &azsecrets.SecretAttributes{ + Enabled: &enable, }, } +} - // Setup client and mock Sender - sender := &mockSender{} - basicClient := keyvault.New() - basicClient.Sender = sender +func makeResponse(id azsecrets.ID, value string, err error) (azsecrets.GetSecretResponse, error) { + return azsecrets.GetSecretResponse{ + Secret: azsecrets.Secret{ + ID: &id, + Value: &value, + }, + }, err +} - // DoFunc returns our mocked data when Do is called - sender.DoFunc = func(r *http.Request) (*http.Response, error) { - u, err := url.Parse(fmt.Sprintf("%s", r.URL)) - if err != nil { - t.Fatalf("expected 0 errors but got: %s", err) - } - if fmt.Sprintf("%s", u.Path) == "/secrets" { - return &http.Response{ - StatusCode: tt["secrets"].StatusCode, - Body: io.NopCloser(strings.NewReader(tt["secrets"].Body)), +func newAzureKeyVaultBackendMock(simulateError string) *backends.AzureKeyVault { + return &backends.AzureKeyVault{ + Credential: nil, + ClientBuilder: func(vaultURL string, credential azcore.TokenCredential, options *azsecrets.ClientOptions) (backends.AzSecretsClient, error) { + return &mockClientProxy{ + simulateError: simulateError, }, nil - } else if fmt.Sprintf("%s", u.Path) == "/secrets/bar/versions" { - return &http.Response{ - StatusCode: tt["foobar"].StatusCode, - Body: io.NopCloser(strings.NewReader(tt["foobar"].Body)), - }, nil - } else if fmt.Sprintf("%s", u.Path) == "/secrets/bar/33740fc26214497f8904d93f20f7db6c" { - return &http.Response{ - StatusCode: tt["bar_version"].StatusCode, - Body: io.NopCloser(strings.NewReader(tt["bar_version"].Body)), - }, nil - } else if fmt.Sprintf("%s", u.Path) == "/secrets/bar/33740fc26214497f8904d93f20f7db6b" { - return &http.Response{ - StatusCode: tt["bar_disabled"].StatusCode, - Body: io.NopCloser(strings.NewReader(tt["bar_disabled"].Body)), - }, nil - } else { - s := strings.Split(u.Path, "/")[2] - return &http.Response{ - StatusCode: tt[s].StatusCode, - Body: io.NopCloser(strings.NewReader(tt[s].Body)), - }, nil - } + }, } +} - kv := backends.NewAzureKeyVaultBackend(basicClient) - - t.Run("Azure retrieve secrets no version", func(t *testing.T) { - - secretList, err := kv.GetSecrets("test", "", map[string]string{}) - if err != nil { - t.Fatalf("expected 0 errors but got: %s", err) - } +func (c *mockClientProxy) NewListSecretPropertiesPager(options *azsecrets.ListSecretPropertiesOptions) *runtime.Pager[azsecrets.ListSecretPropertiesResponse] { + var pageCount = 0 + pager := runtime.NewPager(runtime.PagingHandler[azsecrets.ListSecretPropertiesResponse]{ + More: func(current azsecrets.ListSecretPropertiesResponse) bool { + return pageCount == 0 + }, + Fetcher: func(ctx context.Context, current *azsecrets.ListSecretPropertiesResponse) (azsecrets.ListSecretPropertiesResponse, error) { + pageCount++ + var a []*azsecrets.SecretProperties + if c.simulateError == "fetch_error" { + return azsecrets.ListSecretPropertiesResponse{}, errors.New("fetch error") + } else if c.simulateError == "get_secret_error" { + a = append(a, makeSecretProperties(secretNamePrefix+"invalid/v2", true)) + } + a = append(a, makeSecretProperties(secretNamePrefix+"simple/v2", true)) + a = append(a, makeSecretProperties(secretNamePrefix+"second/v2", true)) + a = append(a, makeSecretProperties(secretNamePrefix+"disabled/v2", false)) + return azsecrets.ListSecretPropertiesResponse{ + SecretPropertiesListResult: azsecrets.SecretPropertiesListResult{ + Value: a, + }, + }, nil + }, + }) + return pager +} - expected := map[string]interface{}{ - "foo": "bar", - "bar": "baz", - } +func (c *mockClientProxy) GetSecret(ctx context.Context, name string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error) { + if name == "simple" && (version == "" || version == "v1") { + return makeResponse(secretNamePrefix+"simple/v1", "a_value_v1", nil) + } else if name == "simple" && version == "v2" { + return makeResponse(secretNamePrefix+"simple/v2", "a_value_v2", nil) + } else if name == "second" && (version == "" || version == "v2") { + return makeResponse(secretNamePrefix+"second/v2", "a_second_value_v2", nil) + } + return makeResponse("", "", errors.New("secret not found")) +} - if !reflect.DeepEqual(expected, secretList) { - t.Errorf("expected: %s, got: %s.", expected, secretList) - } +func TestAzLogin(t *testing.T) { + var keyVault = newAzureKeyVaultBackendMock("") + var err = keyVault.Login() + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } +} - }) +func TestAzGetSecret(t *testing.T) { + var keyVault = newAzureKeyVaultBackendMock("") + var data, err = keyVault.GetIndividualSecret("keyvault", "simple", "", nil) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + expected := "a_value_v1" + if !reflect.DeepEqual(expected, data) { + t.Errorf("expected: %s, got: %s.", expected, data) + } +} - t.Run("Azure retrieve secrets with version", func(t *testing.T) { +func TestAzGetSecretWithVersion(t *testing.T) { + var keyVault = newAzureKeyVaultBackendMock("") + var data, err = keyVault.GetIndividualSecret("keyvault", "simple", "v2", nil) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + expected := "a_value_v2" + if !reflect.DeepEqual(expected, data) { + t.Errorf("expected: %s, got: %s.", expected, data) + } +} - // test version - secretList, err := kv.GetSecrets("test", "33740fc26214497f8904d93f20f7db6c", map[string]string{}) - if err != nil { - t.Fatalf("expected 0 errors but got: %s", err) - } +func TestAzGetSecretWithWrongVersion(t *testing.T) { + var keyVault = newAzureKeyVaultBackendMock("") + var _, err = keyVault.GetIndividualSecret("keyvault", "simple", "v3", nil) + if err == nil { + t.Fatalf("expected 1 errors but got nil") + } + expected := errors.New("secret not found") + if !reflect.DeepEqual(err, expected) { + t.Errorf("expected err: %s, got: %s.", expected, err) + } +} - expected := map[string]interface{}{ - "bar": "baz-version", - } +func TestAzGetSecretNotExist(t *testing.T) { + var keyVault = newAzureKeyVaultBackendMock("") + var _, err = keyVault.GetIndividualSecret("keyvault", "not_existing", "", nil) + if err == nil { + t.Fatalf("expected 1 errors but got nil") + } + expected := errors.New("secret not found") + if !reflect.DeepEqual(err, expected) { + t.Errorf("expected err: %s, got: %s.", expected, err) + } +} - if !reflect.DeepEqual(expected, secretList) { - t.Errorf("expected: %s, got: %s.", expected, secretList) - } +func TestAzGetSecretBuilderError(t *testing.T) { + var keyVault = &backends.AzureKeyVault{ + Credential: nil, + ClientBuilder: func(vaultURL string, credential azcore.TokenCredential, options *azsecrets.ClientOptions) (backends.AzSecretsClient, error) { + return nil, errors.New("boom") + }, + } + var _, err = keyVault.GetIndividualSecret("keyvault", "not_existing", "", nil) + if err == nil { + t.Fatalf("expected 1 errors but got nil") + } + expected := errors.New("boom") + if !reflect.DeepEqual(err, expected) { + t.Errorf("expected err: %s, got: %s.", expected, err) + } +} - }) +func TestAzGetSecrets(t *testing.T) { + var keyVault = newAzureKeyVaultBackendMock("") + var res, err = keyVault.GetSecrets("keyvault", "", nil) - t.Run("Azure GetIndividualSecret", func(t *testing.T) { - secret, err := kv.GetIndividualSecret("test", "bar", "33740fc26214497f8904d93f20f7db6c", map[string]string{}) - if err != nil { - t.Fatalf("expected 0 errors but got: %s", err) - } + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + expected := map[string]interface{}{ + "simple": "a_value_v1", + "second": "a_second_value_v2", + } + if !reflect.DeepEqual(res, expected) { + t.Errorf("expected: %s, got: %s.", expected, res) + } +} - expected := "baz-version" +func TestAzGetSecretsWithError(t *testing.T) { + var keyVault = newAzureKeyVaultBackendMock("fetch_error") + var _, err = keyVault.GetSecrets("keyvault", "", nil) + if err == nil { + t.Fatalf("expected 1 errors but got nil") + } + expected := errors.New("fetch error") + if !reflect.DeepEqual(err, expected) { + t.Errorf("expected err: %s, got: %s.", expected, err) + } +} - if !reflect.DeepEqual(expected, secret) { - t.Errorf("expected: %s, got: %s.", expected, secret) - } - }) +func TestAzGetSecretsWithErrorOnGetSecret(t *testing.T) { + var keyVault = newAzureKeyVaultBackendMock("get_secret_error") + var _, err = keyVault.GetSecrets("keyvault", "", nil) + if err == nil { + t.Fatalf("expected 1 errors but got nil") + } + expected := errors.New("secret not found") + if !reflect.DeepEqual(err, expected) { + t.Errorf("expected err: %s, got: %s.", expected, err) + } +} - t.Run("Azure retrieve secrets with version disabled", func(t *testing.T) { +func TestAzGetSecretsBuilderError(t *testing.T) { + var keyVault = &backends.AzureKeyVault{ + Credential: nil, + ClientBuilder: func(vaultURL string, credential azcore.TokenCredential, options *azsecrets.ClientOptions) (backends.AzSecretsClient, error) { + return nil, errors.New("boom") + }, + } + var _, err = keyVault.GetSecrets("keyvault", "", nil) + if err == nil { + t.Fatalf("expected 1 errors but got nil") + } + expected := errors.New("boom") + if !reflect.DeepEqual(err, expected) { + t.Errorf("expected err: %s, got: %s.", expected, err) + } +} - // test disabled secret - secretList, err := kv.GetSecrets("test", "33740fc26214497f8904d93f20f7db6b", map[string]string{}) - if err != nil { - t.Fatalf("expected 0 errors but got: %s", err) - } +func TestAzGetSecretsVersionV1(t *testing.T) { + var keyVault = newAzureKeyVaultBackendMock("") + var res, err = keyVault.GetSecrets("keyvault", "v1", nil) - expected := map[string]interface{}{} + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + expected := map[string]interface{}{ + "simple": "a_value_v1", + } + if !reflect.DeepEqual(res, expected) { + t.Errorf("expected: %s, got: %s.", expected, res) + } +} - if !reflect.DeepEqual(expected, secretList) { - t.Errorf("expected: %s, got: %s.", expected, secretList) - } +func TestAzGetSecretsVersionV2(t *testing.T) { + var keyVault = newAzureKeyVaultBackendMock("") + var res, err = keyVault.GetSecrets("keyvault", "v2", nil) - }) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + expected := map[string]interface{}{ + "simple": "a_value_v2", + "second": "a_second_value_v2", + } + if !reflect.DeepEqual(res, expected) { + t.Errorf("expected: %s, got: %s.", expected, res) + } } diff --git a/pkg/config/config.go b/pkg/config/config.go index fc07ffb8..bc896701 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" "os" "strconv" "strings" @@ -11,8 +12,7 @@ import ( gcpsm "cloud.google.com/go/secretmanager/apiv1" "github.com/1Password/connect-sdk-go/connect" - "github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault" - kvauth "github.com/Azure/azure-sdk-for-go/services/keyvault/auth" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" delineasecretserver "github.com/DelineaXPM/tss-sdk-go/v2/server" "github.com/IBM/go-sdk-core/v5/core" ibmsm "github.com/IBM/secrets-manager-go-sdk/secretsmanagerv2" @@ -190,14 +190,12 @@ func New(v *viper.Viper, co *Options) (*Config, error) { } case types.AzureKeyVaultbackend: { - authorizer, err := kvauth.NewAuthorizerFromEnvironment() + cred, err := azidentity.NewDefaultAzureCredential(nil) if err != nil { return nil, err } - basicClient := keyvault.New() - basicClient.Authorizer = authorizer - backend = backends.NewAzureKeyVaultBackend(basicClient) + backend = backends.NewAzureKeyVaultBackend(cred, azsecrets.NewClient) } case types.Sopsbackend: { diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 5fa49dad..c7925b9d 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -417,14 +417,6 @@ func TestNewConfigMissingParameter(t *testing.T) { }, "*backends.AWSSecretsManager", }, - { - map[string]interface{}{ - "AVP_TYPE": "azurekeyvault", - "AZURE_TENANT_ID": "test", - "AZURE_CLIENT_ID": "test", - }, - "*backends.AzureKeyVault", - }, { map[string]interface{}{ "AVP_TYPE": "yandexcloudlockbox",