From bbc159833761dabe211dd68dea5612cfd9dd1b42 Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Wed, 23 Feb 2022 01:52:03 +0000 Subject: [PATCH 1/5] Report secret version as a hash of the input and contents --- CHANGELOG.md | 5 + internal/config/config.go | 17 ++ internal/config/config_test.go | 14 +- internal/provider/provider.go | 85 ++++---- internal/provider/provider_test.go | 302 +++++++++++++++++++---------- 5 files changed, 273 insertions(+), 150 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a9a3ba..f8a2b9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +CHANGES: + +* Duplicate object names now trigger an error instead of silently overwriting files. [[GH-148](https://github.com/hashicorp/vault-csi-provider/pull/148)] + IMPROVEMENTS: * New flags to configure default Vault namespace and TLS details. [[GH-138](https://github.com/hashicorp/vault-csi-provider/pull/138)] @@ -10,6 +14,7 @@ IMPROVEMENTS: * `-vault-tls-client-cert` * `-vault-tls-client-key` * `-vault-tls-skip-verify` +* Secret versions are now reported as a hash of their input and contents instead of hardcoded to 0. [[GH-148](https://github.com/hashicorp/vault-csi-provider/pull/148)] ## 1.0.0 (January 25th, 2022) diff --git a/internal/config/config.go b/internal/config/config.go index fd16524..e82b410 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,8 +3,10 @@ package config import ( "encoding/json" "errors" + "fmt" "os" "strconv" + "strings" "github.com/hashicorp/vault/api" "gopkg.in/yaml.v3" @@ -158,5 +160,20 @@ func (c *Config) validate() error { return errors.New("no secrets configured - the provider will not read any secret material") } + objectNames := map[string]struct{}{} + conflicts := []string{} + for _, secret := range c.Parameters.Secrets { + if _, exists := objectNames[secret.ObjectName]; exists { + conflicts = append(conflicts, secret.ObjectName) + } + + objectNames[secret.ObjectName] = struct{}{} + } + + if len(conflicts) > 0 { + return fmt.Errorf("Each `objectName` within a SecretProviderClass must be unique, "+ + "but the following keys were duplicated: %s", strings.Join(conflicts, ", ")) + } + return nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 020f23d..c0e71a1 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -3,6 +3,7 @@ package config import ( "encoding/json" "io/ioutil" + "net/http" "path/filepath" "testing" @@ -92,7 +93,7 @@ func TestParseParameters(t *testing.T) { Insecure: true, }, Secrets: []Secret{ - {"bar1", "v1/secret/foo1", "", "GET", nil}, + {"bar1", "v1/secret/foo1", "", http.MethodGet, nil}, {"bar2", "v1/secret/foo2", "", "", nil}, }, PodInfo: PodInfo{ @@ -265,6 +266,17 @@ func TestValidateConfig(t *testing.T) { return cfg }(), }, + { + name: "Duplicate objectName", + cfg: func() Config { + cfg := minimumValid + cfg.Parameters.Secrets = []Secret{ + {ObjectName: "foo", SecretPath: "path/one"}, + {ObjectName: "foo", SecretPath: "path/two"}, + } + return cfg + }(), + }, } { err := tc.cfg.validate() if tc.cfgValid { diff --git a/internal/provider/provider.go b/internal/provider/provider.go index b15c886..598f870 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -2,12 +2,12 @@ package provider import ( "context" + "crypto/sha256" + "encoding/base64" "encoding/json" "fmt" - "io/ioutil" + "net/http" "net/url" - "os" - "path/filepath" "strings" "time" @@ -27,12 +27,16 @@ import ( type provider struct { logger hclog.Logger cache map[cacheKey]*api.Secret + + // Allows mocking Kubernetes API for tests. + k8sClientConfig func() (*rest.Config, error) } func NewProvider(logger hclog.Logger) *provider { p := &provider{ - logger: logger, - cache: make(map[cacheKey]*api.Secret), + logger: logger, + cache: make(map[cacheKey]*api.Secret), + k8sClientConfig: rest.InClusterConfig, } return p @@ -50,7 +54,7 @@ func (p *provider) createJWTToken(ctx context.Context, podInfo config.PodInfo) ( "podName", podInfo.Name, "podUID", podInfo.UID) - config, err := rest.InClusterConfig() + config, err := p.k8sClientConfig() if err != nil { return "", err } @@ -81,31 +85,31 @@ func (p *provider) createJWTToken(ctx context.Context, podInfo config.PodInfo) ( return resp.Status.Token, nil } -func (p *provider) login(ctx context.Context, client *api.Client, params config.Parameters) (string, error) { +func (p *provider) login(ctx context.Context, client *api.Client, params config.Parameters) error { p.logger.Debug("performing vault login") jwt, err := p.createJWTToken(ctx, params.PodInfo) if err != nil { - return "", err + return err } - req := client.NewRequest("POST", "/v1/auth/"+params.VaultKubernetesMountPath+"/login") + req := client.NewRequest(http.MethodPost, "/v1/auth/"+params.VaultKubernetesMountPath+"/login") err = req.SetJSONBody(map[string]string{ "role": params.VaultRoleName, "jwt": jwt, }) if err != nil { - return "", err + return err } secret, err := vaultclient.Do(ctx, client, req) if err != nil { - return "", fmt.Errorf("failed to login: %w", err) + return fmt.Errorf("failed to login: %w", err) } client.SetToken(secret.Auth.ClientToken) p.logger.Debug("vault login successful") - return secret.Auth.ClientToken, nil + return nil } func ensureV1Prefix(s string) string { @@ -133,7 +137,7 @@ func generateRequest(client *api.Client, secret config.Secret) (*api.Request, er } secretPath = secretPath[:queryIndex] } - method := "GET" + method := http.MethodGet if secret.Method != "" { method = secret.Method } @@ -228,8 +232,6 @@ func (p *provider) getSecret(ctx context.Context, client *api.Client, secretConf // MountSecretsStoreObjectContent mounts content of the vault object to target path func (p *provider) HandleMountRequest(ctx context.Context, cfg config.Config, flagsConfig config.FlagsConfig) (*pb.MountResponse, error) { - versions := make(map[string]string) - client, err := vaultclient.New(cfg.Parameters, flagsConfig) if err != nil { return nil, err @@ -241,59 +243,46 @@ func (p *provider) HandleMountRequest(ctx context.Context, cfg config.Config, fl } // Authenticate to vault using the jwt token - _, err = p.login(ctx, client, cfg.Parameters) + err = p.login(ctx, client, cfg.Parameters) if err != nil { return nil, err } var files []*pb.File + var objectVersions []*pb.ObjectVersion for _, secret := range cfg.Parameters.Secrets { content, err := p.getSecret(ctx, client, secret) if err != nil { return nil, err } - versions[fmt.Sprintf("%s:%s:%s", secret.ObjectName, secret.SecretPath, secret.Method)] = "0" + + version, err := generateObjectVersion(secret, content) + if err != nil { + return nil, fmt.Errorf("failed to generate version for object name %q: %w", secret.ObjectName, err) + } files = append(files, &pb.File{Path: secret.ObjectName, Mode: int32(cfg.FilePermission), Contents: content}) + objectVersions = append(objectVersions, version) p.logger.Info("secret added to mount response", "directory", cfg.TargetPath, "file", secret.ObjectName) } - var ov []*pb.ObjectVersion - for k, v := range versions { - ov = append(ov, &pb.ObjectVersion{Id: k, Version: v}) - } - return &pb.MountResponse{ - ObjectVersion: ov, Files: files, + ObjectVersion: objectVersions, }, nil } -func writeSecret(logger hclog.Logger, directory string, file string, content []byte, permission os.FileMode) error { - if err := validateFilePath(file); err != nil { - return err - } - if filepath.Base(file) != file { - err := os.MkdirAll(filepath.Join(directory, filepath.Dir(file)), 0755) - if err != nil { - return err - } - } - if err := ioutil.WriteFile(filepath.Join(directory, file), content, permission); err != nil { - return fmt.Errorf("secrets-store csi driver failed to write %s at %s: %w", file, directory, err) - } - logger.Info("secrets-store csi driver wrote secret", "directory", directory, "file", file) - - return nil -} - -func validateFilePath(path string) error { - segments := strings.Split(strings.ReplaceAll(path, `\`, "/"), "/") - for _, segment := range segments { - if segment == ".." { - return fmt.Errorf("ObjectName %q invalid, must not contain any .. segments", path) - } +func generateObjectVersion(secret config.Secret, content []byte) (*pb.ObjectVersion, error) { + hash := sha256.New() + // We include the secret config in the hash input to avoid leaking information + // about different secrets that could have the same content. + _, err := hash.Write([]byte(fmt.Sprintf("%v:%s", secret, content))) + if err != nil { + return nil, err } - return nil + return &pb.ObjectVersion{ + Id: secret.ObjectName, + Version: base64.URLEncoding.EncodeToString(hash.Sum(nil)), + }, nil } diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index e64cefa..5a74df8 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -1,10 +1,11 @@ package provider import ( - "io/ioutil" - "os" - "path/filepath" - "strings" + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" "testing" "github.com/hashicorp/go-hclog" @@ -12,100 +13,11 @@ import ( "github.com/hashicorp/vault/api" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "k8s.io/client-go/rest" + "sigs.k8s.io/secrets-store-csi-driver/provider/v1alpha1" + pb "sigs.k8s.io/secrets-store-csi-driver/provider/v1alpha1" ) -func TestValidateFilePath(t *testing.T) { - // Don't use filepath.Join to generate the test cases because it calls filepath.Clean - // which simplifies some of the test cases into less interesting paths. - for _, tc := range []string{ - "", - ".", - "/", - "bar", - "bar/foo", - "bar///foo", - "./bar", - "/foo/bar", - "foo/bar\\baz", - } { - err := validateFilePath(tc) - if err != nil { - t.Fatalf("Expected no error for %q but got %s", tc, err) - } - } -} - -func TestValidatePath_Malformed(t *testing.T) { - for _, tc := range []string{ - "../bar", - "foo/..", - "foo/../../bar", - "foo////..", - } { - err := validateFilePath(tc) - if err == nil { - t.Fatalf("Expected error for %q but got none", tc) - } - - tc = strings.ReplaceAll(tc, "/", "\\") - err = validateFilePath(tc) - if err == nil { - t.Fatalf("Expected error for %q but got none", tc) - } - } -} - -func TestWriteSecret(t *testing.T) { - l := hclog.NewNullLogger() - for _, tc := range []struct { - name string - file string - permission os.FileMode - invalid bool - }{ - { - name: "simple case", - file: "foo", - permission: 0644, - }, - { - name: "validation error", - file: filepath.Join("..", "foo"), - permission: 0644, - invalid: true, - }, - { - name: "requires new directory", - file: filepath.Join("foo", "bar", "baz"), - permission: 0644, - }, - { - name: "only owner can read", - file: "foo", - permission: 0600, - }, - } { - root, err := ioutil.TempDir(os.TempDir(), "") - require.NoError(t, err, tc.name) - defer func() { - require.NoError(t, os.RemoveAll(root), tc.name) - }() - - err = writeSecret(l, root, tc.file, nil, tc.permission) - if tc.invalid { - require.Error(t, err, tc.name) - assert.Contains(t, err.Error(), "must not contain any .. segments", tc.name) - continue - } - - require.NoError(t, err, tc.name) - rootedPath := filepath.Join(root, tc.file) - info, err := os.Stat(rootedPath) - require.NoError(t, err, tc.name) - assert.Equal(t, tc.permission, info.Mode(), tc.name) - } -} - func TestEnsureV1Prefix(t *testing.T) { for _, tc := range []struct { name string @@ -146,21 +58,21 @@ func TestGenerateRequest(t *testing.T) { secret: config.Secret{ SecretPath: "secret/foo", }, - expected: expected{"GET", "/v1/secret/foo", "", ""}, + expected: expected{http.MethodGet, "/v1/secret/foo", "", ""}, }, { name: "zero-length query string", secret: config.Secret{ SecretPath: "secret/foo?", }, - expected: expected{"GET", "/v1/secret/foo", "", ""}, + expected: expected{http.MethodGet, "/v1/secret/foo", "", ""}, }, { name: "query string", secret: config.Secret{ SecretPath: "secret/foo?bar=true&baz=maybe&zap=0", }, - expected: expected{"GET", "/v1/secret/foo", "bar=true&baz=maybe&zap=0", ""}, + expected: expected{http.MethodGet, "/v1/secret/foo", "bar=true&baz=maybe&zap=0", ""}, }, { name: "method specified", @@ -174,14 +86,14 @@ func TestGenerateRequest(t *testing.T) { name: "body specified", secret: config.Secret{ SecretPath: "secret/foo", - Method: "POST", + Method: http.MethodPost, SecretArgs: map[string]interface{}{ "bar": true, "baz": 10, "zap": "a string", }, }, - expected: expected{"POST", "/v1/secret/foo", "", `{"bar":true,"baz":10,"zap":"a string"}`}, + expected: expected{http.MethodPost, "/v1/secret/foo", "", `{"bar":true,"baz":10,"zap":"a string"}`}, }, } { req, err := generateRequest(client, tc.secret) @@ -277,3 +189,191 @@ func TestKeyFromData(t *testing.T) { assert.Equal(t, tc.expected, content) } } + +func TestHandleMountRequest(t *testing.T) { + // SETUP + mockKubernetesServer := httptest.NewServer(http.HandlerFunc(mockKubernetesHandler)) + defer mockKubernetesServer.Close() + + mockVaultServer := httptest.NewServer(http.HandlerFunc(mockVaultHandler())) + defer mockVaultServer.Close() + + // Generate a fresh provider on each request to avoid hitting the cache. + provider := func() *provider { + p := NewProvider(hclog.Default()) + p.k8sClientConfig = func() (*rest.Config, error) { + return &rest.Config{ + Host: mockKubernetesServer.URL, + }, nil + } + return p + } + + spcConfig := config.Config{ + TargetPath: "some/unused/path", + FilePermission: 0, + Parameters: config.Parameters{ + VaultRoleName: "my-vault-role", + Secrets: []config.Secret{ + { + ObjectName: "object-one", + SecretPath: "path/one", + SecretKey: "the-key", + Method: "", + SecretArgs: nil, + }, + { + ObjectName: "object-two", + SecretPath: "path/two", + SecretKey: "", + Method: "", + SecretArgs: nil, + }, + }, + PodInfo: config.PodInfo{ + ServiceAccountName: "a-k8s-service-account", + Namespace: "a-k8s-namespace", + Name: "a-pod", + UID: "a-uid", + }, + }, + } + flagsConfig := config.FlagsConfig{ + VaultAddr: mockVaultServer.URL, + } + + // TEST + expectedFiles := []*pb.File{ + { + Path: "object-one", + Mode: 0, + Contents: []byte("secret v1 from: /v1/path/one"), + }, + { + Path: "object-two", + Mode: 0, + Contents: []byte(`{"request_id":"","lease_id":"","lease_duration":0,"renewable":false,"data":{"the-key":"secret v1 from: /v1/path/two"},"warnings":null}`), + }, + } + expectedVersions := []*pb.ObjectVersion{ + { + Id: "object-one", + Version: "GTA4Q4qmllcXGP5c-2zGpd2nDKA_koge-LpAK3QS6x4=", + }, + { + Id: "object-two", + Version: "01yGq-JMHV5hkbN-VeaV0sqmhXSigHwkSa1-xiLByLQ=", + }, + } + + // While we hit the cache, the secret contents and versions should remain the same. + p := provider() + for i := 0; i < 3; i++ { + resp, err := p.HandleMountRequest(context.Background(), spcConfig, flagsConfig) + require.NoError(t, err) + + assert.Equal(t, (*v1alpha1.Error)(nil), resp.Error) + assert.Equal(t, expectedFiles, resp.Files) + assert.Equal(t, expectedVersions, resp.ObjectVersion) + } + + // Mounting again with a fresh provider will update the contents of the secrets, which should update the version. + resp, err := provider().HandleMountRequest(context.Background(), spcConfig, flagsConfig) + require.NoError(t, err) + + assert.Equal(t, (*v1alpha1.Error)(nil), resp.Error) + expectedFiles[0].Contents = []byte("secret v2 from: /v1/path/one") + expectedFiles[1].Contents = []byte(`{"request_id":"","lease_id":"","lease_duration":0,"renewable":false,"data":{"the-key":"secret v2 from: /v1/path/two"},"warnings":null}`) + expectedVersions[0].Version = "pEVsjkL1Sa3izLS3yl5jUz3nVdgWbWi4kX5sH-WqYvQ=" + expectedVersions[1].Version = "YhyNECvv1klLks1FxzC690cgBncilNwc5G-UlwIRNDY=" + assert.Equal(t, expectedFiles, resp.Files) + assert.Equal(t, expectedVersions, resp.ObjectVersion) +} + +func mockKubernetesHandler(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, err := w.Write([]byte(tokenRequestResponse)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +func mockVaultHandler() func(w http.ResponseWriter, req *http.Request) { + getsPerPath := map[string]int{} + + return func(w http.ResponseWriter, req *http.Request) { + switch req.Method { + case http.MethodPost: + // Assume all POSTs are login requests and return a token. + body, err := json.Marshal(&api.Secret{ + Auth: &api.SecretAuth{ + ClientToken: "my-vault-client-token", + }, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, err = w.Write(body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + case http.MethodGet: + // Assume all GETs are secret reads and return a derivative of the request path. + path := req.URL.Path + getsPerPath[path]++ + body, err := json.Marshal(&api.Secret{ + Data: map[string]interface{}{ + "the-key": fmt.Sprintf("secret v%d from: %s", getsPerPath[path], path), + }, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, err = w.Write(body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + } +} + +// To regenerate, configure kubectl for a cluster (e.g. `kind create cluster`), and run: +// kubectl proxy & +// curl --silent http://127.0.0.1:8001/api/v1/namespaces/default/serviceaccounts/default/token \ +// -H "Content-Type: application/json" \ +// -X POST \ +// -d '{"apiVersion": "authentication.k8s.io/v1", "kind": "TokenRequest"}' +// kill %% +const tokenRequestResponse = `{ + "kind": "TokenRequest", + "apiVersion": "authentication.k8s.io/v1", + "metadata": { + "creationTimestamp": null, + "managedFields": [ + { + "manager": "curl", + "operation": "Update", + "apiVersion": "authentication.k8s.io/v1", + "time": "2022-02-22T15:28:56Z", + "fieldsType": "FieldsV1", + "fieldsV1": {"f:spec":{"f:expirationSeconds":{}}} + } + ] + }, + "spec": { + "audiences": [ + "https://kubernetes.default.svc.cluster.local" + ], + "expirationSeconds": 3600, + "boundObjectRef": null + }, + "status": { + "token": "a-kubernetes-jwt", + "expirationTimestamp": "2022-02-22T16:28:56Z" + } +}` From d6414b70d9beea31b42b251d181e1fc98f0ee481 Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Thu, 24 Feb 2022 12:04:01 +0000 Subject: [PATCH 2/5] Use fake k8s client in provider test --- go.sum | 8 ++++++ internal/provider/provider.go | 22 +++++----------- internal/provider/provider_test.go | 42 +++++++----------------------- internal/server/server.go | 7 +++-- main.go | 13 ++++++++- 5 files changed, 41 insertions(+), 51 deletions(-) diff --git a/go.sum b/go.sum index defd13c..50e1579 100644 --- a/go.sum +++ b/go.sum @@ -160,6 +160,7 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.11.0+incompatible h1:glyUF9yIYtMHzn8xaKw5rMhdWcwsYV8dZHIq5567/xs= github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -174,6 +175,7 @@ github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= @@ -476,6 +478,7 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.5/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= @@ -491,6 +494,7 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -498,6 +502,7 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ= +github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -530,6 +535,7 @@ github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -1018,6 +1024,7 @@ gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76 gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= @@ -1071,6 +1078,7 @@ k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/klog/v2 v2.10.0 h1:R2HDMDJsHVTHA2n4RjwbeYXdOcBymXdX/JRb1v0VGhE= k8s.io/klog/v2 v2.10.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= +k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e h1:KLHHjkdQFomZy8+06csTWZ0m1343QqxZhR2LJ1OxCYM= k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= k8s.io/mount-utils v0.21.0/go.mod h1:dwXbIPxKtTjrBEaX1aK/CMEf1KZ8GzMHpe3NEBfdFXI= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 598f870..9bcef73 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -18,7 +18,6 @@ import ( authenticationv1 "k8s.io/api/authentication/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" pb "sigs.k8s.io/secrets-store-csi-driver/provider/v1alpha1" ) @@ -29,14 +28,14 @@ type provider struct { cache map[cacheKey]*api.Secret // Allows mocking Kubernetes API for tests. - k8sClientConfig func() (*rest.Config, error) + k8sClient kubernetes.Interface } -func NewProvider(logger hclog.Logger) *provider { +func NewProvider(logger hclog.Logger, k8sClient kubernetes.Interface) *provider { p := &provider{ - logger: logger, - cache: make(map[cacheKey]*api.Secret), - k8sClientConfig: rest.InClusterConfig, + logger: logger, + cache: make(map[cacheKey]*api.Secret), + k8sClient: k8sClient, } return p @@ -54,17 +53,8 @@ func (p *provider) createJWTToken(ctx context.Context, podInfo config.PodInfo) ( "podName", podInfo.Name, "podUID", podInfo.UID) - config, err := p.k8sClientConfig() - if err != nil { - return "", err - } - clientset, err := kubernetes.NewForConfig(config) - if err != nil { - return "", err - } - ttl := int64((15 * time.Minute).Seconds()) - resp, err := clientset.CoreV1().ServiceAccounts(podInfo.Namespace).CreateToken(ctx, podInfo.ServiceAccountName, &authenticationv1.TokenRequest{ + resp, err := p.k8sClient.CoreV1().ServiceAccounts(podInfo.Namespace).CreateToken(ctx, podInfo.ServiceAccountName, &authenticationv1.TokenRequest{ Spec: authenticationv1.TokenRequestSpec{ ExpirationSeconds: &ttl, // TODO: Support audiences as a configurable. diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 5a74df8..86ebaab 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -13,7 +13,9 @@ import ( "github.com/hashicorp/vault/api" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "k8s.io/client-go/rest" + authenticationv1 "k8s.io/api/authentication/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes/fake" "sigs.k8s.io/secrets-store-csi-driver/provider/v1alpha1" pb "sigs.k8s.io/secrets-store-csi-driver/provider/v1alpha1" ) @@ -192,22 +194,13 @@ func TestKeyFromData(t *testing.T) { func TestHandleMountRequest(t *testing.T) { // SETUP - mockKubernetesServer := httptest.NewServer(http.HandlerFunc(mockKubernetesHandler)) - defer mockKubernetesServer.Close() - mockVaultServer := httptest.NewServer(http.HandlerFunc(mockVaultHandler())) defer mockVaultServer.Close() - // Generate a fresh provider on each request to avoid hitting the cache. - provider := func() *provider { - p := NewProvider(hclog.Default()) - p.k8sClientConfig = func() (*rest.Config, error) { - return &rest.Config{ - Host: mockKubernetesServer.URL, - }, nil - } - return p - } + k8sClient := fake.NewSimpleClientset( + &corev1.ServiceAccount{}, + &authenticationv1.TokenRequest{}, + ) spcConfig := config.Config{ TargetPath: "some/unused/path", @@ -230,12 +223,6 @@ func TestHandleMountRequest(t *testing.T) { SecretArgs: nil, }, }, - PodInfo: config.PodInfo{ - ServiceAccountName: "a-k8s-service-account", - Namespace: "a-k8s-namespace", - Name: "a-pod", - UID: "a-uid", - }, }, } flagsConfig := config.FlagsConfig{ @@ -267,9 +254,9 @@ func TestHandleMountRequest(t *testing.T) { } // While we hit the cache, the secret contents and versions should remain the same. - p := provider() + provider := NewProvider(hclog.Default(), k8sClient) for i := 0; i < 3; i++ { - resp, err := p.HandleMountRequest(context.Background(), spcConfig, flagsConfig) + resp, err := provider.HandleMountRequest(context.Background(), spcConfig, flagsConfig) require.NoError(t, err) assert.Equal(t, (*v1alpha1.Error)(nil), resp.Error) @@ -278,7 +265,7 @@ func TestHandleMountRequest(t *testing.T) { } // Mounting again with a fresh provider will update the contents of the secrets, which should update the version. - resp, err := provider().HandleMountRequest(context.Background(), spcConfig, flagsConfig) + resp, err := NewProvider(hclog.Default(), k8sClient).HandleMountRequest(context.Background(), spcConfig, flagsConfig) require.NoError(t, err) assert.Equal(t, (*v1alpha1.Error)(nil), resp.Error) @@ -290,15 +277,6 @@ func TestHandleMountRequest(t *testing.T) { assert.Equal(t, expectedVersions, resp.ObjectVersion) } -func mockKubernetesHandler(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - _, err := w.Write([]byte(tokenRequestResponse)) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} - func mockVaultHandler() func(w http.ResponseWriter, req *http.Request) { getsPerPath := map[string]int{} diff --git a/internal/server/server.go b/internal/server/server.go index 85022a5..e7d5c91 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/vault-csi-provider/internal/config" "github.com/hashicorp/vault-csi-provider/internal/provider" "github.com/hashicorp/vault-csi-provider/internal/version" + "k8s.io/client-go/kubernetes" pb "sigs.k8s.io/secrets-store-csi-driver/provider/v1alpha1" ) @@ -19,12 +20,14 @@ var ( type Server struct { logger hclog.Logger flagsConfig config.FlagsConfig + k8sClient kubernetes.Interface } -func NewServer(logger hclog.Logger, flagsConfig config.FlagsConfig) *Server { +func NewServer(logger hclog.Logger, flagsConfig config.FlagsConfig, k8sClient kubernetes.Interface) *Server { return &Server{ logger: logger, flagsConfig: flagsConfig, + k8sClient: k8sClient, } } @@ -42,7 +45,7 @@ func (s *Server) Mount(ctx context.Context, req *pb.MountRequest) (*pb.MountResp return nil, err } - provider := provider.NewProvider(s.logger.Named("provider")) + provider := provider.NewProvider(s.logger.Named("provider"), s.k8sClient) resp, err := provider.HandleMountRequest(ctx, cfg, s.flagsConfig) if err != nil { return nil, fmt.Errorf("error making mount request: %w", err) diff --git a/main.go b/main.go index 0f790ef..b3733cc 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,8 @@ import ( "github.com/hashicorp/vault-csi-provider/internal/version" "google.golang.org/grpc" "google.golang.org/grpc/status" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" pb "sigs.k8s.io/secrets-store-csi-driver/provider/v1alpha1" ) @@ -91,7 +93,16 @@ func realMain(logger hclog.Logger) error { } defer listener.Close() - srv := providerserver.NewServer(serverLogger, flags) + config, err := rest.InClusterConfig() + if err != nil { + return err + } + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return err + } + + srv := providerserver.NewServer(serverLogger, flags, clientset) pb.RegisterCSIDriverProviderServer(server, srv) // Create health handler From 1465d8162500f4f62f04a62d834869428a4cf521 Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Fri, 25 Feb 2022 14:10:35 +0000 Subject: [PATCH 3/5] Delete unused --- internal/provider/provider_test.go | 36 ------------------------------ 1 file changed, 36 deletions(-) diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 86ebaab..bd6b323 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -319,39 +319,3 @@ func mockVaultHandler() func(w http.ResponseWriter, req *http.Request) { } } } - -// To regenerate, configure kubectl for a cluster (e.g. `kind create cluster`), and run: -// kubectl proxy & -// curl --silent http://127.0.0.1:8001/api/v1/namespaces/default/serviceaccounts/default/token \ -// -H "Content-Type: application/json" \ -// -X POST \ -// -d '{"apiVersion": "authentication.k8s.io/v1", "kind": "TokenRequest"}' -// kill %% -const tokenRequestResponse = `{ - "kind": "TokenRequest", - "apiVersion": "authentication.k8s.io/v1", - "metadata": { - "creationTimestamp": null, - "managedFields": [ - { - "manager": "curl", - "operation": "Update", - "apiVersion": "authentication.k8s.io/v1", - "time": "2022-02-22T15:28:56Z", - "fieldsType": "FieldsV1", - "fieldsV1": {"f:spec":{"f:expirationSeconds":{}}} - } - ] - }, - "spec": { - "audiences": [ - "https://kubernetes.default.svc.cluster.local" - ], - "expirationSeconds": 3600, - "boundObjectRef": null - }, - "status": { - "token": "a-kubernetes-jwt", - "expirationTimestamp": "2022-02-22T16:28:56Z" - } -}` From b4289312f5650f2e3a08c1fd0c52f76c0706affc Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Wed, 4 May 2022 12:13:16 +0100 Subject: [PATCH 4/5] Update expected versions for new file permission input, improve test comment --- internal/provider/provider_test.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index bd6b323..f90a8f5 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -245,11 +245,11 @@ func TestHandleMountRequest(t *testing.T) { expectedVersions := []*pb.ObjectVersion{ { Id: "object-one", - Version: "GTA4Q4qmllcXGP5c-2zGpd2nDKA_koge-LpAK3QS6x4=", + Version: "NUAYElpND6QqTB7MYXdP_kCAjsXQTxCO24ExLXXsKPk=", }, { Id: "object-two", - Version: "01yGq-JMHV5hkbN-VeaV0sqmhXSigHwkSa1-xiLByLQ=", + Version: "2x2gbSKY0Ot2c9RW8djcD9o3oGuSbwZ1ZXzvnp8ArZg=", }, } @@ -264,15 +264,16 @@ func TestHandleMountRequest(t *testing.T) { assert.Equal(t, expectedVersions, resp.ObjectVersion) } - // Mounting again with a fresh provider will update the contents of the secrets, which should update the version. + // The mockVaultHandler function below includes a dynamic counter in the content of secrets. + // That means mounting again with a fresh provider will update the contents of the secrets, which should update the version. resp, err := NewProvider(hclog.Default(), k8sClient).HandleMountRequest(context.Background(), spcConfig, flagsConfig) require.NoError(t, err) assert.Equal(t, (*v1alpha1.Error)(nil), resp.Error) expectedFiles[0].Contents = []byte("secret v2 from: /v1/path/one") expectedFiles[1].Contents = []byte(`{"request_id":"","lease_id":"","lease_duration":0,"renewable":false,"data":{"the-key":"secret v2 from: /v1/path/two"},"warnings":null}`) - expectedVersions[0].Version = "pEVsjkL1Sa3izLS3yl5jUz3nVdgWbWi4kX5sH-WqYvQ=" - expectedVersions[1].Version = "YhyNECvv1klLks1FxzC690cgBncilNwc5G-UlwIRNDY=" + expectedVersions[0].Version = "_MwvWQq78rNEsiDtzGPtECvgHmCi2EhlXc6Sdmcemhw=" + expectedVersions[1].Version = "9Ck2wFZxO5vGIY08Pk_RNSfR8dJh-_QB4ev3KSCDXOg=" assert.Equal(t, expectedFiles, resp.Files) assert.Equal(t, expectedVersions, resp.ObjectVersion) } From 168bf8cf348e53d3962e2026da2ea02a4499205c Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Wed, 4 May 2022 16:59:06 +0100 Subject: [PATCH 5/5] Update internal/config/config.go Co-authored-by: Theron Voran --- internal/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/config/config.go b/internal/config/config.go index 5a57146..5983254 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -174,7 +174,7 @@ func (c *Config) validate() error { } if len(conflicts) > 0 { - return fmt.Errorf("Each `objectName` within a SecretProviderClass must be unique, "+ + return fmt.Errorf("each `objectName` within a SecretProviderClass must be unique, "+ "but the following keys were duplicated: %s", strings.Join(conflicts, ", ")) }