mirror of
https://github.com/external-secrets/external-secrets.git
synced 2024-12-14 11:57:59 +00:00
ed173dcf77
Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com> Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>
336 lines
10 KiB
Go
336 lines
10 KiB
Go
/*
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package keyvault
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Azure/go-autorest/autorest"
|
|
"github.com/Azure/go-autorest/autorest/adal"
|
|
tassert "github.com/stretchr/testify/assert"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/utils/pointer"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
|
|
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
|
v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
|
|
utilfake "github.com/external-secrets/external-secrets/pkg/provider/util/fake"
|
|
)
|
|
|
|
var vaultURL = "https://local.vault.url"
|
|
|
|
func TestNewClientManagedIdentityNoNeedForCredentials(t *testing.T) {
|
|
namespace := "internal"
|
|
identityID := "1234"
|
|
authType := esv1beta1.AzureManagedIdentity
|
|
store := esv1beta1.SecretStore{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: namespace,
|
|
},
|
|
Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{AzureKV: &esv1beta1.AzureKVProvider{
|
|
AuthType: &authType,
|
|
IdentityID: &identityID,
|
|
VaultURL: &vaultURL,
|
|
}}},
|
|
}
|
|
k8sClient := clientfake.NewClientBuilder().Build()
|
|
az := &Azure{
|
|
crClient: k8sClient,
|
|
namespace: namespace,
|
|
provider: store.Spec.Provider.AzureKV,
|
|
store: &store,
|
|
}
|
|
authorizer, err := az.authorizerForManagedIdentity()
|
|
if err != nil {
|
|
// On non Azure environment, MSI auth not available, so this error should be returned
|
|
tassert.EqualError(t, err, "failed to get oauth token from MSI: MSI not available")
|
|
} else {
|
|
// On Azure (where GitHub Actions are running) a secretClient is returned, as only an Authorizer is configured, but no token is requested for MI
|
|
tassert.NotNil(t, authorizer)
|
|
}
|
|
}
|
|
|
|
func TestGetAuthorizorForWorkloadIdentity(t *testing.T) {
|
|
const (
|
|
tenantID = "my-tenant-id"
|
|
clientID = "my-client-id"
|
|
azAccessToken = "my-access-token"
|
|
saToken = "FAKETOKEN"
|
|
saName = "az-wi"
|
|
namespace = "default"
|
|
)
|
|
|
|
// create a temporary file to imitate
|
|
// azure workload identity webhook
|
|
// see AZURE_FEDERATED_TOKEN_FILE
|
|
tf, err := os.CreateTemp("", "")
|
|
tassert.Nil(t, err)
|
|
defer os.RemoveAll(tf.Name())
|
|
_, err = tf.WriteString(saToken)
|
|
tassert.Nil(t, err)
|
|
tokenFile := tf.Name()
|
|
|
|
authType := esv1beta1.AzureWorkloadIdentity
|
|
defaultProvider := &esv1beta1.AzureKVProvider{
|
|
VaultURL: &vaultURL,
|
|
AuthType: &authType,
|
|
ServiceAccountRef: &v1.ServiceAccountSelector{
|
|
Name: saName,
|
|
},
|
|
}
|
|
|
|
type testCase struct {
|
|
name string
|
|
provider *esv1beta1.AzureKVProvider
|
|
k8sObjects []client.Object
|
|
prep func(*testing.T)
|
|
expErr string
|
|
}
|
|
|
|
for _, row := range []testCase{
|
|
{
|
|
name: "missing service account",
|
|
provider: defaultProvider,
|
|
expErr: "serviceaccounts \"" + saName + "\" not found",
|
|
},
|
|
{
|
|
name: "missing webhook env vars",
|
|
provider: &esv1beta1.AzureKVProvider{},
|
|
expErr: "missing environment variables. AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_FEDERATED_TOKEN_FILE must be set",
|
|
},
|
|
{
|
|
name: "missing workload identity token file",
|
|
provider: &esv1beta1.AzureKVProvider{},
|
|
prep: func(t *testing.T) {
|
|
t.Setenv("AZURE_CLIENT_ID", clientID)
|
|
t.Setenv("AZURE_TENANT_ID", tenantID)
|
|
t.Setenv("AZURE_FEDERATED_TOKEN_FILE", "invalid file")
|
|
},
|
|
expErr: "unable to read token file invalid file: open invalid file: no such file or directory",
|
|
},
|
|
{
|
|
name: "correct workload identity",
|
|
provider: &esv1beta1.AzureKVProvider{},
|
|
prep: func(t *testing.T) {
|
|
t.Setenv("AZURE_CLIENT_ID", clientID)
|
|
t.Setenv("AZURE_TENANT_ID", tenantID)
|
|
t.Setenv("AZURE_FEDERATED_TOKEN_FILE", tokenFile)
|
|
},
|
|
},
|
|
{
|
|
name: "missing sa annotations",
|
|
provider: defaultProvider,
|
|
k8sObjects: []client.Object{
|
|
&corev1.ServiceAccount{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: saName,
|
|
Namespace: namespace,
|
|
Annotations: map[string]string{},
|
|
},
|
|
},
|
|
},
|
|
expErr: "missing service account annotation: azure.workload.identity/client-id",
|
|
},
|
|
{
|
|
name: "successful case",
|
|
provider: defaultProvider,
|
|
k8sObjects: []client.Object{
|
|
&corev1.ServiceAccount{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: saName,
|
|
Namespace: namespace,
|
|
Annotations: map[string]string{
|
|
AnnotationClientID: clientID,
|
|
AnnotationTenantID: tenantID,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} {
|
|
t.Run(row.name, func(t *testing.T) {
|
|
store := esv1beta1.SecretStore{
|
|
Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{
|
|
AzureKV: row.provider,
|
|
}},
|
|
}
|
|
k8sClient := clientfake.NewClientBuilder().
|
|
WithObjects(row.k8sObjects...).
|
|
Build()
|
|
az := &Azure{
|
|
store: &store,
|
|
namespace: namespace,
|
|
crClient: k8sClient,
|
|
kubeClient: utilfake.NewCreateTokenMock().WithToken(saToken),
|
|
provider: store.Spec.Provider.AzureKV,
|
|
}
|
|
tokenProvider := func(ctx context.Context, token, clientID, tenantID, aadEndpoint, kvResource string) (adal.OAuthTokenProvider, error) {
|
|
tassert.Equal(t, token, saToken)
|
|
tassert.Equal(t, clientID, clientID)
|
|
tassert.Equal(t, tenantID, tenantID)
|
|
return &tokenProvider{accessToken: azAccessToken}, nil
|
|
}
|
|
if row.prep != nil {
|
|
row.prep(t)
|
|
}
|
|
authorizer, err := az.authorizerForWorkloadIdentity(context.Background(), tokenProvider)
|
|
if row.expErr == "" {
|
|
tassert.NotNil(t, authorizer)
|
|
tassert.Equal(t, getTokenFromAuthorizer(t, authorizer), azAccessToken)
|
|
} else {
|
|
tassert.EqualError(t, err, row.expErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuth(t *testing.T) {
|
|
defaultStore := esv1beta1.SecretStore{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "default",
|
|
},
|
|
Spec: esv1beta1.SecretStoreSpec{
|
|
Provider: &esv1beta1.SecretStoreProvider{},
|
|
},
|
|
}
|
|
authType := esv1beta1.AzureServicePrincipal
|
|
|
|
type testCase struct {
|
|
name string
|
|
provider *esv1beta1.AzureKVProvider
|
|
store esv1beta1.GenericStore
|
|
objects []client.Object
|
|
expErr string
|
|
}
|
|
for _, row := range []testCase{
|
|
{
|
|
name: "bad config",
|
|
expErr: "missing secretRef in provider config",
|
|
store: &defaultStore,
|
|
provider: &esv1beta1.AzureKVProvider{
|
|
AuthType: &authType,
|
|
VaultURL: &vaultURL,
|
|
TenantID: pointer.String("mytenant"),
|
|
},
|
|
},
|
|
{
|
|
name: "bad config",
|
|
expErr: "missing accessKeyID/secretAccessKey in store config",
|
|
store: &defaultStore,
|
|
provider: &esv1beta1.AzureKVProvider{
|
|
AuthType: &authType,
|
|
VaultURL: &vaultURL,
|
|
TenantID: pointer.String("mytenant"),
|
|
AuthSecretRef: &esv1beta1.AzureKVAuth{},
|
|
},
|
|
},
|
|
{
|
|
name: "bad config: missing secret",
|
|
expErr: "could not find secret default/password: secrets \"password\" not found",
|
|
store: &defaultStore,
|
|
provider: &esv1beta1.AzureKVProvider{
|
|
AuthType: &authType,
|
|
VaultURL: &vaultURL,
|
|
TenantID: pointer.String("mytenant"),
|
|
AuthSecretRef: &esv1beta1.AzureKVAuth{
|
|
ClientSecret: &v1.SecretKeySelector{Name: "password"},
|
|
ClientID: &v1.SecretKeySelector{Name: "password"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "cluster secret store",
|
|
expErr: "could not find secret foo/password: secrets \"password\" not found",
|
|
store: &esv1beta1.ClusterSecretStore{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: esv1beta1.ClusterSecretStoreKind,
|
|
},
|
|
Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{}},
|
|
},
|
|
provider: &esv1beta1.AzureKVProvider{
|
|
AuthType: &authType,
|
|
VaultURL: &vaultURL,
|
|
TenantID: pointer.String("mytenant"),
|
|
AuthSecretRef: &esv1beta1.AzureKVAuth{
|
|
ClientSecret: &v1.SecretKeySelector{Name: "password", Namespace: pointer.String("foo")},
|
|
ClientID: &v1.SecretKeySelector{Name: "password", Namespace: pointer.String("foo")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "correct cluster secret store",
|
|
objects: []client.Object{&corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "password",
|
|
Namespace: "foo",
|
|
},
|
|
Data: map[string][]byte{
|
|
"id": []byte("foo"),
|
|
"secret": []byte("bar"),
|
|
},
|
|
}},
|
|
store: &esv1beta1.ClusterSecretStore{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: esv1beta1.ClusterSecretStoreKind,
|
|
},
|
|
Spec: esv1beta1.SecretStoreSpec{Provider: &esv1beta1.SecretStoreProvider{}},
|
|
},
|
|
provider: &esv1beta1.AzureKVProvider{
|
|
AuthType: &authType,
|
|
VaultURL: &vaultURL,
|
|
TenantID: pointer.String("mytenant"),
|
|
AuthSecretRef: &esv1beta1.AzureKVAuth{
|
|
ClientSecret: &v1.SecretKeySelector{Name: "password", Namespace: pointer.String("foo"), Key: "secret"},
|
|
ClientID: &v1.SecretKeySelector{Name: "password", Namespace: pointer.String("foo"), Key: "id"},
|
|
},
|
|
},
|
|
},
|
|
} {
|
|
t.Run(row.name, func(t *testing.T) {
|
|
k8sClient := clientfake.NewClientBuilder().WithObjects(row.objects...).Build()
|
|
spec := row.store.GetSpec()
|
|
spec.Provider.AzureKV = row.provider
|
|
az := &Azure{
|
|
crClient: k8sClient,
|
|
namespace: "default",
|
|
provider: spec.Provider.AzureKV,
|
|
store: row.store,
|
|
}
|
|
authorizer, err := az.authorizerForServicePrincipal(context.Background())
|
|
if row.expErr == "" {
|
|
tassert.Nil(t, err)
|
|
tassert.NotNil(t, authorizer)
|
|
} else {
|
|
tassert.EqualError(t, err, row.expErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func getTokenFromAuthorizer(t *testing.T, authorizer autorest.Authorizer) string {
|
|
rq, _ := http.NewRequest("POST", "http://example.com", http.NoBody)
|
|
_, err := authorizer.WithAuthorization()(
|
|
autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
|
return rq, nil
|
|
})).Prepare(rq)
|
|
tassert.Nil(t, err)
|
|
return strings.TrimPrefix(rq.Header.Get("Authorization"), "Bearer ")
|
|
}
|