Browse Source

ref(kubernetes): move auth package from kubernetes to esutils (#6149)

Co-authored-by: Gergely Bräutigam <gergely.brautigam@sap.com>
Alexander Chernov 2 weeks ago
parent
commit
f152b86d7a
3 changed files with 357 additions and 132 deletions
  1. 5 118
      providers/v1/kubernetes/auth.go
  2. 196 0
      runtime/esutils/k8s_rest_config.go
  3. 156 14
      providers/v1/kubernetes/auth_test.go

+ 5 - 118
providers/v1/kubernetes/auth.go

@@ -20,134 +20,21 @@ package kubernetes
 
 import (
 	"context"
-	"errors"
-	"fmt"
 
-	authenticationv1 "k8s.io/api/authentication/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/rest"
-	"k8s.io/client-go/tools/clientcmd"
 
-	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
-	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 	"github.com/external-secrets/external-secrets/runtime/esutils"
-	"github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
-)
-
-const (
-	errUnableCreateToken = "cannot create service account token: %q"
 )
 
 func (c *Client) getAuth(ctx context.Context) (*rest.Config, error) {
-	if c.store.AuthRef != nil {
-		cfg, err := c.fetchSecretKey(ctx, *c.store.AuthRef)
-		if err != nil {
-			return nil, err
-		}
-
-		return clientcmd.RESTConfigFromKubeConfig(cfg)
-	}
-
-	if c.store.Auth == nil {
-		return nil, errors.New("no auth provider given")
-	}
-
-	if c.store.Server.URL == "" {
-		return nil, errors.New("no server URL provided")
-	}
-
-	cfg := &rest.Config{
-		Host: c.store.Server.URL,
-	}
-
-	ca, err := esutils.FetchCACertFromSource(ctx, esutils.CreateCertOpts{
-		CABundle:   c.store.Server.CABundle,
-		CAProvider: c.store.Server.CAProvider,
-		StoreKind:  c.storeKind,
-		Namespace:  c.namespace,
-		Client:     c.ctrlClient,
-	})
-	if err != nil {
-		return nil, err
-	}
-
-	cfg.TLSClientConfig = rest.TLSClientConfig{
-		Insecure: false,
-		CAData:   ca,
-	}
-
-	switch {
-	case c.store.Auth.Token != nil:
-		token, err := c.fetchSecretKey(ctx, c.store.Auth.Token.BearerToken)
-		if err != nil {
-			return nil, fmt.Errorf("could not fetch Auth.Token.BearerToken: %w", err)
-		}
-
-		cfg.BearerToken = string(token)
-	case c.store.Auth.ServiceAccount != nil:
-		token, err := c.serviceAccountToken(ctx, c.store.Auth.ServiceAccount)
-		if err != nil {
-			return nil, fmt.Errorf("could not fetch Auth.ServiceAccount: %w", err)
-		}
-
-		cfg.BearerToken = string(token)
-	case c.store.Auth.Cert != nil:
-		key, cert, err := c.getClientKeyAndCert(ctx)
-		if err != nil {
-			return nil, fmt.Errorf("could not fetch client key and cert: %w", err)
-		}
-
-		cfg.TLSClientConfig.KeyData = key
-		cfg.TLSClientConfig.CertData = cert
-	default:
-		return nil, errors.New("no auth provider given")
-	}
-
-	return cfg, nil
-}
-
-func (c *Client) getClientKeyAndCert(ctx context.Context) ([]byte, []byte, error) {
-	var err error
-	cert, err := c.fetchSecretKey(ctx, c.store.Auth.Cert.ClientCert)
-	if err != nil {
-		return nil, nil, fmt.Errorf("unable to fetch client certificate: %w", err)
-	}
-	key, err := c.fetchSecretKey(ctx, c.store.Auth.Cert.ClientKey)
-	if err != nil {
-		return nil, nil, fmt.Errorf("unable to fetch client key: %w", err)
-	}
-	return key, cert, nil
-}
-
-func (c *Client) serviceAccountToken(ctx context.Context, serviceAccountRef *esmeta.ServiceAccountSelector) ([]byte, error) {
-	namespace := c.namespace
-	if (c.storeKind == esv1.ClusterSecretStoreKind) &&
-		(serviceAccountRef.Namespace != nil) {
-		namespace = *serviceAccountRef.Namespace
-	}
-	expirationSeconds := int64(3600)
-	tr, err := c.ctrlClientset.ServiceAccounts(namespace).CreateToken(ctx, serviceAccountRef.Name, &authenticationv1.TokenRequest{
-		Spec: authenticationv1.TokenRequestSpec{
-			Audiences:         serviceAccountRef.Audiences,
-			ExpirationSeconds: &expirationSeconds,
-		},
-	}, metav1.CreateOptions{})
-	if err != nil {
-		return nil, fmt.Errorf(errUnableCreateToken, err)
-	}
-	return []byte(tr.Status.Token), nil
-}
-
-func (c *Client) fetchSecretKey(ctx context.Context, ref esmeta.SecretKeySelector) ([]byte, error) {
-	secret, err := resolvers.SecretKeyRef(
+	return esutils.BuildRESTConfigFromKubernetesConnection(
 		ctx,
 		c.ctrlClient,
+		c.ctrlClientset,
 		c.storeKind,
 		c.namespace,
-		&ref,
+		c.store.Server,
+		c.store.Auth,
+		c.store.AuthRef,
 	)
-	if err != nil {
-		return nil, err
-	}
-	return []byte(secret), nil
 }

+ 196 - 0
runtime/esutils/k8s_rest_config.go

@@ -0,0 +1,196 @@
+/*
+Copyright © The ESO Authors
+
+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
+
+    https://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 esutils
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	authenticationv1 "k8s.io/api/authentication/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/clientcmd"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
+)
+
+const errUnableCreateK8sSAToken = "cannot create service account token: %q"
+
+// ErrMultipleAuthMethods is returned when more than one inline auth method is configured.
+var ErrMultipleAuthMethods = errors.New("multiple auth methods provided: set exactly one of Token, ServiceAccount, or Cert")
+
+// ErrAuthRefWithInlineAuth is returned when both authRef and inline auth are set.
+var ErrAuthRefWithInlineAuth = errors.New("authRef and inline auth cannot both be set")
+
+// BuildRESTConfigFromKubernetesConnection builds a *rest.Config from the same
+// server/auth fields used by the Kubernetes SecretStore provider. It is shared
+// by the kubernetes and CRD providers.
+func BuildRESTConfigFromKubernetesConnection(
+	ctx context.Context,
+	ctrlClient kclient.Client,
+	coreV1 typedcorev1.CoreV1Interface,
+	storeKind, esNamespace string,
+	server esv1.KubernetesServer,
+	auth *esv1.KubernetesAuth,
+	authRef *esmeta.SecretKeySelector,
+) (*rest.Config, error) {
+	if authRef != nil {
+		if auth != nil {
+			return nil, ErrAuthRefWithInlineAuth
+		}
+		cfg, err := fetchKubernetesSecretKey(ctx, ctrlClient, storeKind, esNamespace, *authRef)
+		if err != nil {
+			return nil, err
+		}
+		return clientcmd.RESTConfigFromKubeConfig(cfg)
+	}
+
+	if auth == nil {
+		return nil, errors.New("no auth provider given")
+	}
+
+	if server.URL == "" {
+		return nil, errors.New("no server URL provided")
+	}
+
+	cfg := &rest.Config{
+		Host: server.URL,
+	}
+
+	ca, err := FetchCACertFromSource(ctx, CreateCertOpts{
+		CABundle:   server.CABundle,
+		CAProvider: server.CAProvider,
+		StoreKind:  storeKind,
+		Namespace:  esNamespace,
+		Client:     ctrlClient,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	cfg.TLSClientConfig = rest.TLSClientConfig{
+		Insecure: false,
+		CAData:   ca,
+	}
+
+	if err := applyInlineAuth(ctx, ctrlClient, coreV1, storeKind, esNamespace, auth, cfg); err != nil {
+		return nil, err
+	}
+	return cfg, nil
+}
+
+// applyInlineAuth validates that exactly one auth method is set and populates cfg accordingly.
+func applyInlineAuth(
+	ctx context.Context,
+	ctrlClient kclient.Client,
+	coreV1 typedcorev1.CoreV1Interface,
+	storeKind, esNamespace string,
+	auth *esv1.KubernetesAuth,
+	cfg *rest.Config,
+) error {
+	set := 0
+	if auth.Token != nil {
+		set++
+	}
+	if auth.ServiceAccount != nil {
+		set++
+	}
+	if auth.Cert != nil {
+		set++
+	}
+	if set > 1 {
+		return ErrMultipleAuthMethods
+	}
+
+	switch {
+	case auth.Token != nil:
+		token, err := fetchKubernetesSecretKey(ctx, ctrlClient, storeKind, esNamespace, auth.Token.BearerToken)
+		if err != nil {
+			return fmt.Errorf("could not fetch Auth.Token.BearerToken: %w", err)
+		}
+		cfg.BearerToken = string(token)
+	case auth.ServiceAccount != nil:
+		token, err := serviceAccountToken(ctx, coreV1, storeKind, esNamespace, auth.ServiceAccount)
+		if err != nil {
+			return fmt.Errorf("could not fetch Auth.ServiceAccount: %w", err)
+		}
+		cfg.BearerToken = string(token)
+	case auth.Cert != nil:
+		key, cert, err := clientCertKeyFromSecrets(ctx, ctrlClient, storeKind, esNamespace, auth.Cert)
+		if err != nil {
+			return fmt.Errorf("could not fetch client key and cert: %w", err)
+		}
+		cfg.TLSClientConfig.KeyData = key
+		cfg.TLSClientConfig.CertData = cert
+	default:
+		return errors.New("no auth provider given")
+	}
+	return nil
+}
+
+// fetchKubernetesSecretKey resolves a SecretKeySelector and returns its value as bytes.
+func fetchKubernetesSecretKey(ctx context.Context, ctrlClient kclient.Client, storeKind, esNamespace string, ref esmeta.SecretKeySelector) ([]byte, error) {
+	secret, err := resolvers.SecretKeyRef(
+		ctx,
+		ctrlClient,
+		storeKind,
+		esNamespace,
+		&ref,
+	)
+	if err != nil {
+		return nil, err
+	}
+	return []byte(secret), nil
+}
+
+// serviceAccountToken creates a short-lived token for the referenced service account.
+// For ClusterSecretStore, serviceAccountRef.namespace overrides the provided namespace.
+func serviceAccountToken(ctx context.Context, coreV1 typedcorev1.CoreV1Interface, storeKind, namespace string, serviceAccountRef *esmeta.ServiceAccountSelector) ([]byte, error) {
+	if storeKind == esv1.ClusterSecretStoreKind && serviceAccountRef.Namespace != nil {
+		namespace = *serviceAccountRef.Namespace
+	}
+	expirationSeconds := int64(3600)
+	tr, err := coreV1.ServiceAccounts(namespace).CreateToken(ctx, serviceAccountRef.Name, &authenticationv1.TokenRequest{
+		Spec: authenticationv1.TokenRequestSpec{
+			Audiences:         serviceAccountRef.Audiences,
+			ExpirationSeconds: &expirationSeconds,
+		},
+	}, metav1.CreateOptions{})
+	if err != nil {
+		return nil, fmt.Errorf(errUnableCreateK8sSAToken, err)
+	}
+	return []byte(tr.Status.Token), nil
+}
+
+// clientCertKeyFromSecrets fetches client certificate and key material from Secrets.
+// It returns key bytes first, then certificate bytes to match rest.TLSClientConfig fields.
+func clientCertKeyFromSecrets(ctx context.Context, ctrlClient kclient.Client, storeKind, esNamespace string, cert *esv1.CertAuth) ([]byte, []byte, error) {
+	certPEM, err := fetchKubernetesSecretKey(ctx, ctrlClient, storeKind, esNamespace, cert.ClientCert)
+	if err != nil {
+		return nil, nil, fmt.Errorf("unable to fetch client certificate: %w", err)
+	}
+	keyPEM, err := fetchKubernetesSecretKey(ctx, ctrlClient, storeKind, esNamespace, cert.ClientKey)
+	if err != nil {
+		return nil, nil, fmt.Errorf("unable to fetch client key: %w", err)
+	}
+	return keyPEM, certPEM, nil
+}

+ 156 - 14
providers/v1/kubernetes/auth_test.go

@@ -14,10 +14,11 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package kubernetes
+package esutils
 
 import (
 	"context"
+	"errors"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -71,7 +72,7 @@ users:
 	serverURL = "https://my.test.tld"
 )
 
-func TestSetAuth(t *testing.T) {
+func TestBuildRESTConfigFromKubernetesConnection(t *testing.T) {
 	type fields struct {
 		kube          kclient.Client
 		kubeclientset typedcorev1.CoreV1Interface
@@ -81,10 +82,11 @@ func TestSetAuth(t *testing.T) {
 	}
 	type want = rest.Config
 	tests := []struct {
-		name    string
-		fields  fields
-		want    *want
-		wantErr bool
+		name      string
+		fields    fields
+		want      *want
+		wantErr   bool
+		wantErrIs error
 	}{
 		{
 			name: "should return err if no ca provided",
@@ -379,6 +381,137 @@ func TestSetAuth(t *testing.T) {
 			wantErr: true,
 		},
 		{
+			name: "should error when Token and ServiceAccount are both set",
+			fields: fields{
+				namespace: "default",
+				kube:      fclient.NewClientBuilder().Build(),
+				store: &esv1.KubernetesProvider{
+					Server: esv1.KubernetesServer{
+						URL:      serverURL,
+						CABundle: []byte(caCert),
+					},
+					Auth: &esv1.KubernetesAuth{
+						Token: &esv1.TokenAuth{
+							BearerToken: v1.SecretKeySelector{Name: "tok", Key: "token"},
+						},
+						ServiceAccount: &v1.ServiceAccountSelector{Name: "my-sa"},
+					},
+				},
+			},
+			want:      nil,
+			wantErr:   true,
+			wantErrIs: ErrMultipleAuthMethods,
+		},
+		{
+			name: "should error when Token and Cert are both set",
+			fields: fields{
+				namespace: "default",
+				kube:      fclient.NewClientBuilder().Build(),
+				store: &esv1.KubernetesProvider{
+					Server: esv1.KubernetesServer{
+						URL:      serverURL,
+						CABundle: []byte(caCert),
+					},
+					Auth: &esv1.KubernetesAuth{
+						Token: &esv1.TokenAuth{
+							BearerToken: v1.SecretKeySelector{Name: "tok", Key: "token"},
+						},
+						Cert: &esv1.CertAuth{
+							ClientCert: v1.SecretKeySelector{Name: "mycert", Key: "cert"},
+							ClientKey:  v1.SecretKeySelector{Name: "mycert", Key: "key"},
+						},
+					},
+				},
+			},
+			want:      nil,
+			wantErr:   true,
+			wantErrIs: ErrMultipleAuthMethods,
+		},
+		{
+			name: "should error when ServiceAccount and Cert are both set",
+			fields: fields{
+				namespace: "default",
+				kube:      fclient.NewClientBuilder().Build(),
+				store: &esv1.KubernetesProvider{
+					Server: esv1.KubernetesServer{
+						URL:      serverURL,
+						CABundle: []byte(caCert),
+					},
+					Auth: &esv1.KubernetesAuth{
+						ServiceAccount: &v1.ServiceAccountSelector{Name: "my-sa"},
+						Cert: &esv1.CertAuth{
+							ClientCert: v1.SecretKeySelector{Name: "mycert", Key: "cert"},
+							ClientKey:  v1.SecretKeySelector{Name: "mycert", Key: "key"},
+						},
+					},
+				},
+			},
+			want:      nil,
+			wantErr:   true,
+			wantErrIs: ErrMultipleAuthMethods,
+		},
+		{
+			name: "should error when all three auth methods are set",
+			fields: fields{
+				namespace: "default",
+				kube:      fclient.NewClientBuilder().Build(),
+				store: &esv1.KubernetesProvider{
+					Server: esv1.KubernetesServer{
+						URL:      serverURL,
+						CABundle: []byte(caCert),
+					},
+					Auth: &esv1.KubernetesAuth{
+						Token: &esv1.TokenAuth{
+							BearerToken: v1.SecretKeySelector{Name: "tok", Key: "token"},
+						},
+						ServiceAccount: &v1.ServiceAccountSelector{Name: "my-sa"},
+						Cert: &esv1.CertAuth{
+							ClientCert: v1.SecretKeySelector{Name: "mycert", Key: "cert"},
+							ClientKey:  v1.SecretKeySelector{Name: "mycert", Key: "key"},
+						},
+					},
+				},
+			},
+			want:      nil,
+			wantErr:   true,
+			wantErrIs: ErrMultipleAuthMethods,
+		},
+		{
+			name: "should error when authRef and inline auth are both set",
+			fields: fields{
+				namespace: "default",
+				kube: fclient.NewClientBuilder().WithObjects(&corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "foobar",
+						Namespace: "default",
+					},
+					Data: map[string][]byte{
+						"config": []byte(authTestKubeConfig),
+						"token":  []byte("mytoken"),
+					},
+				}).Build(),
+				store: &esv1.KubernetesProvider{
+					Server: esv1.KubernetesServer{
+						URL:      serverURL,
+						CABundle: []byte(caCert),
+					},
+					Auth: &esv1.KubernetesAuth{
+						Token: &esv1.TokenAuth{
+							BearerToken: v1.SecretKeySelector{Name: "foobar", Key: "token"},
+						},
+					},
+					AuthRef: &v1.SecretKeySelector{
+						Name:      "foobar",
+						Namespace: new("default"),
+						Key:       "config",
+					},
+				},
+			},
+			want:      nil,
+			wantErr:   true,
+			wantErrIs: ErrAuthRefWithInlineAuth,
+		},
+		{
 			name: "should read config from secret",
 			fields: fields{
 				namespace: "default",
@@ -411,16 +544,25 @@ func TestSetAuth(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			k := &Client{
-				ctrlClientset: tt.fields.kubeclientset,
-				ctrlClient:    tt.fields.kube,
-				store:         tt.fields.store,
-				namespace:     tt.fields.namespace,
-				storeKind:     tt.fields.storeKind,
+			storeKind := tt.fields.storeKind
+			if storeKind == "" {
+				storeKind = esv1.SecretStoreKind
 			}
-			cfg, err := k.getAuth(context.Background())
+			cfg, err := BuildRESTConfigFromKubernetesConnection(
+				context.Background(),
+				tt.fields.kube,
+				tt.fields.kubeclientset,
+				storeKind,
+				tt.fields.namespace,
+				tt.fields.store.Server,
+				tt.fields.store.Auth,
+				tt.fields.store.AuthRef,
+			)
 			if (err != nil) != tt.wantErr {
-				t.Errorf("BaseClient.setAuth() error = %v, wantErr %v", err, tt.wantErr)
+				t.Errorf("BuildRESTConfigFromKubernetesConnection() error = %v, wantErr %v", err, tt.wantErr)
+			}
+			if tt.wantErrIs != nil {
+				assert.True(t, errors.Is(err, tt.wantErrIs), "expected errors.Is(%v, %v)", err, tt.wantErrIs)
 			}
 			assert.Equal(t, tt.want, cfg)
 		})