| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package template |
| |
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "net/http" |
| "net/http/httptest" |
| "os" |
| "strings" |
| "testing" |
| "time" |
| |
| ctconfig "github.com/hashicorp/consul-template/config" |
| "github.com/hashicorp/go-hclog" |
| "github.com/hashicorp/vault/command/agent/config" |
| "github.com/hashicorp/vault/command/agent/internal/ctmanager" |
| "github.com/hashicorp/vault/command/agentproxyshared" |
| "github.com/hashicorp/vault/internalshared/configutil" |
| "github.com/hashicorp/vault/internalshared/listenerutil" |
| "github.com/hashicorp/vault/sdk/helper/logging" |
| "github.com/hashicorp/vault/sdk/helper/pointerutil" |
| "github.com/stretchr/testify/assert" |
| "github.com/stretchr/testify/require" |
| "google.golang.org/grpc/test/bufconn" |
| ) |
| |
| func newRunnerConfig(s *ServerConfig, configs ctconfig.TemplateConfigs) (*ctconfig.Config, error) { |
| managerCfg := ctmanager.ManagerConfig{ |
| AgentConfig: s.AgentConfig, |
| } |
| cfg, err := ctmanager.NewConfig(managerCfg, configs) |
| return cfg, err |
| } |
| |
| // TestNewServer is a simple test to make sure NewServer returns a Server and |
| // channel |
| func TestNewServer(t *testing.T) { |
| server := NewServer(&ServerConfig{}) |
| if server == nil { |
| t.Fatal("nil server returned") |
| } |
| } |
| |
| func newAgentConfig(listeners []*configutil.Listener, enableCache, enablePersisentCache bool) *config.Config { |
| agentConfig := &config.Config{ |
| SharedConfig: &configutil.SharedConfig{ |
| PidFile: "./pidfile", |
| Listeners: listeners, |
| }, |
| AutoAuth: &config.AutoAuth{ |
| Method: &config.Method{ |
| Type: "aws", |
| MountPath: "auth/aws", |
| Config: map[string]interface{}{ |
| "role": "foobar", |
| }, |
| }, |
| Sinks: []*config.Sink{ |
| { |
| Type: "file", |
| DHType: "curve25519", |
| DHPath: "/tmp/file-foo-dhpath", |
| AAD: "foobar", |
| Config: map[string]interface{}{ |
| "path": "/tmp/file-foo", |
| }, |
| }, |
| }, |
| }, |
| Vault: &config.Vault{ |
| Address: "http://127.0.0.1:1111", |
| CACert: "config_ca_cert", |
| CAPath: "config_ca_path", |
| TLSSkipVerifyRaw: interface{}("true"), |
| TLSSkipVerify: true, |
| ClientCert: "config_client_cert", |
| ClientKey: "config_client_key", |
| }, |
| } |
| if enableCache { |
| agentConfig.Cache = &config.Cache{ |
| UseAutoAuthToken: true, |
| } |
| } |
| |
| if enablePersisentCache { |
| agentConfig.Cache.Persist = &agentproxyshared.PersistConfig{Type: "kubernetes"} |
| } |
| |
| return agentConfig |
| } |
| |
| func TestCacheConfig(t *testing.T) { |
| listeners := []*configutil.Listener{ |
| { |
| Type: "tcp", |
| Address: "127.0.0.1:8300", |
| TLSDisable: true, |
| }, |
| { |
| Type: "unix", |
| Address: "foobar", |
| TLSDisable: true, |
| SocketMode: "configmode", |
| SocketUser: "configuser", |
| SocketGroup: "configgroup", |
| }, |
| { |
| Type: "tcp", |
| Address: "127.0.0.1:8400", |
| TLSKeyFile: "/path/to/cakey.pem", |
| TLSCertFile: "/path/to/cacert.pem", |
| }, |
| } |
| |
| cases := map[string]struct { |
| cacheEnabled bool |
| persistentCacheEnabled bool |
| setDialer bool |
| expectedErr string |
| expectCustomDialer bool |
| }{ |
| "persistent_cache": { |
| cacheEnabled: true, |
| persistentCacheEnabled: true, |
| setDialer: true, |
| expectedErr: "", |
| expectCustomDialer: true, |
| }, |
| "memory_cache": { |
| cacheEnabled: true, |
| persistentCacheEnabled: false, |
| setDialer: true, |
| expectedErr: "", |
| expectCustomDialer: true, |
| }, |
| "no_cache": { |
| cacheEnabled: false, |
| persistentCacheEnabled: false, |
| setDialer: false, |
| expectedErr: "", |
| expectCustomDialer: false, |
| }, |
| "cache_no_dialer": { |
| cacheEnabled: true, |
| persistentCacheEnabled: false, |
| setDialer: false, |
| expectedErr: "missing in-process dialer configuration", |
| expectCustomDialer: false, |
| }, |
| } |
| |
| for name, tc := range cases { |
| t.Run(name, func(t *testing.T) { |
| agentConfig := newAgentConfig(listeners, tc.cacheEnabled, tc.persistentCacheEnabled) |
| if tc.setDialer && tc.cacheEnabled { |
| bListener := bufconn.Listen(1024 * 1024) |
| defer bListener.Close() |
| agentConfig.Cache.InProcDialer = listenerutil.NewBufConnWrapper(bListener) |
| } |
| serverConfig := ServerConfig{AgentConfig: agentConfig} |
| |
| ctConfig, err := newRunnerConfig(&serverConfig, ctconfig.TemplateConfigs{}) |
| if len(tc.expectedErr) > 0 { |
| require.Error(t, err, tc.expectedErr) |
| return |
| } |
| |
| require.NoError(t, err) |
| require.NotNil(t, ctConfig) |
| assert.Equal(t, tc.expectCustomDialer, ctConfig.Vault.Transport.CustomDialer != nil) |
| |
| if tc.expectCustomDialer { |
| assert.Equal(t, "http://127.0.0.1:8200", *ctConfig.Vault.Address) |
| } else { |
| assert.Equal(t, "http://127.0.0.1:1111", *ctConfig.Vault.Address) |
| } |
| }) |
| } |
| } |
| |
| func TestCacheConfigNoListener(t *testing.T) { |
| listeners := []*configutil.Listener{} |
| |
| agentConfig := newAgentConfig(listeners, true, true) |
| bListener := bufconn.Listen(1024 * 1024) |
| defer bListener.Close() |
| agentConfig.Cache.InProcDialer = listenerutil.NewBufConnWrapper(bListener) |
| serverConfig := ServerConfig{AgentConfig: agentConfig} |
| |
| ctConfig, err := newRunnerConfig(&serverConfig, ctconfig.TemplateConfigs{}) |
| if err != nil { |
| t.Fatalf("unexpected error: %s", err) |
| } |
| |
| assert.Equal(t, "http://127.0.0.1:8200", *ctConfig.Vault.Address) |
| assert.NotNil(t, ctConfig.Vault.Transport.CustomDialer) |
| } |
| |
| func createHttpTestServer() *httptest.Server { |
| // create http test server |
| mux := http.NewServeMux() |
| mux.HandleFunc("/v1/kv/myapp/config", func(w http.ResponseWriter, r *http.Request) { |
| fmt.Fprintln(w, jsonResponse) |
| }) |
| mux.HandleFunc("/v1/kv/myapp/config-bad", func(w http.ResponseWriter, r *http.Request) { |
| w.WriteHeader(404) |
| fmt.Fprintln(w, `{"errors":[]}`) |
| }) |
| mux.HandleFunc("/v1/kv/myapp/perm-denied", func(w http.ResponseWriter, r *http.Request) { |
| w.WriteHeader(403) |
| fmt.Fprintln(w, `{"errors":["1 error occurred:\n\t* permission denied\n\n"]}`) |
| }) |
| |
| return httptest.NewServer(mux) |
| } |
| |
| func TestServerRun(t *testing.T) { |
| ts := createHttpTestServer() |
| defer ts.Close() |
| |
| tmpDir, err := os.MkdirTemp("", "agent-tests") |
| defer os.RemoveAll(tmpDir) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // secretRender is a simple struct that represents the secret we render to |
| // disk. It's used to unmarshal the file contents and test against |
| type secretRender struct { |
| Username string `json:"username"` |
| Password string `json:"password"` |
| Version string `json:"version"` |
| } |
| |
| type templateTest struct { |
| template *ctconfig.TemplateConfig |
| } |
| |
| testCases := map[string]struct { |
| templateMap map[string]*templateTest |
| expectedValues *secretRender |
| expectError bool |
| exitOnRetryFailure bool |
| }{ |
| "simple": { |
| templateMap: map[string]*templateTest{ |
| "render_01": { |
| template: &ctconfig.TemplateConfig{ |
| Contents: pointerutil.StringPtr(templateContents), |
| }, |
| }, |
| }, |
| expectError: false, |
| exitOnRetryFailure: false, |
| }, |
| "multiple": { |
| templateMap: map[string]*templateTest{ |
| "render_01": { |
| template: &ctconfig.TemplateConfig{ |
| Contents: pointerutil.StringPtr(templateContents), |
| }, |
| }, |
| "render_02": { |
| template: &ctconfig.TemplateConfig{ |
| Contents: pointerutil.StringPtr(templateContents), |
| }, |
| }, |
| "render_03": { |
| template: &ctconfig.TemplateConfig{ |
| Contents: pointerutil.StringPtr(templateContents), |
| }, |
| }, |
| "render_04": { |
| template: &ctconfig.TemplateConfig{ |
| Contents: pointerutil.StringPtr(templateContents), |
| }, |
| }, |
| "render_05": { |
| template: &ctconfig.TemplateConfig{ |
| Contents: pointerutil.StringPtr(templateContents), |
| }, |
| }, |
| "render_06": { |
| template: &ctconfig.TemplateConfig{ |
| Contents: pointerutil.StringPtr(templateContents), |
| }, |
| }, |
| "render_07": { |
| template: &ctconfig.TemplateConfig{ |
| Contents: pointerutil.StringPtr(templateContents), |
| }, |
| }, |
| }, |
| expectError: false, |
| exitOnRetryFailure: false, |
| }, |
| "bad secret": { |
| templateMap: map[string]*templateTest{ |
| "render_01": { |
| template: &ctconfig.TemplateConfig{ |
| Contents: pointerutil.StringPtr(templateContentsBad), |
| }, |
| }, |
| }, |
| expectError: true, |
| exitOnRetryFailure: true, |
| }, |
| "missing key": { |
| templateMap: map[string]*templateTest{ |
| "render_01": { |
| template: &ctconfig.TemplateConfig{ |
| Contents: pointerutil.StringPtr(templateContentsMissingKey), |
| ErrMissingKey: pointerutil.BoolPtr(true), |
| }, |
| }, |
| }, |
| expectError: true, |
| exitOnRetryFailure: true, |
| }, |
| "permission denied": { |
| templateMap: map[string]*templateTest{ |
| "render_01": { |
| template: &ctconfig.TemplateConfig{ |
| Contents: pointerutil.StringPtr(templateContentsPermDenied), |
| }, |
| }, |
| }, |
| expectError: true, |
| exitOnRetryFailure: true, |
| }, |
| "with sprig functions": { |
| templateMap: map[string]*templateTest{ |
| "render_01": { |
| template: &ctconfig.TemplateConfig{ |
| Contents: pointerutil.StringPtr(templateContentsWithSprigFunctions), |
| }, |
| }, |
| }, |
| expectedValues: &secretRender{ |
| Username: "APPUSER", |
| Password: "passphrase", |
| Version: "3", |
| }, |
| expectError: false, |
| exitOnRetryFailure: true, |
| }, |
| } |
| |
| for name, tc := range testCases { |
| t.Run(name, func(t *testing.T) { |
| templateTokenCh := make(chan string, 1) |
| var templatesToRender []*ctconfig.TemplateConfig |
| for fileName, templateTest := range tc.templateMap { |
| dstFile := fmt.Sprintf("%s/%s", tmpDir, fileName) |
| templateTest.template.Destination = pointerutil.StringPtr(dstFile) |
| templatesToRender = append(templatesToRender, templateTest.template) |
| } |
| |
| ctx, _ := context.WithTimeout(context.Background(), 20*time.Second) |
| sc := ServerConfig{ |
| Logger: logging.NewVaultLogger(hclog.Trace), |
| AgentConfig: &config.Config{ |
| Vault: &config.Vault{ |
| Address: ts.URL, |
| Retry: &config.Retry{ |
| NumRetries: 3, |
| }, |
| }, |
| TemplateConfig: &config.TemplateConfig{ |
| ExitOnRetryFailure: tc.exitOnRetryFailure, |
| }, |
| }, |
| LogLevel: hclog.Trace, |
| LogWriter: hclog.DefaultOutput, |
| ExitAfterAuth: true, |
| } |
| |
| var server *Server |
| server = NewServer(&sc) |
| if ts == nil { |
| t.Fatal("nil server returned") |
| } |
| |
| errCh := make(chan error) |
| go func() { |
| errCh <- server.Run(ctx, templateTokenCh, templatesToRender) |
| }() |
| |
| // send a dummy value to trigger the internal Runner to query for secret |
| // info |
| templateTokenCh <- "test" |
| |
| select { |
| case <-ctx.Done(): |
| t.Fatal("timeout reached before templates were rendered") |
| case err := <-errCh: |
| if err != nil && !tc.expectError { |
| t.Fatalf("did not expect error, got: %v", err) |
| } |
| if err != nil && tc.expectError { |
| t.Logf("received expected error: %v", err) |
| return |
| } |
| } |
| |
| // verify test file exists and has the content we're looking for |
| var fileCount int |
| var errs []string |
| for _, template := range templatesToRender { |
| if template.Destination == nil { |
| t.Fatal("nil template destination") |
| } |
| content, err := os.ReadFile(*template.Destination) |
| if err != nil { |
| errs = append(errs, err.Error()) |
| continue |
| } |
| fileCount++ |
| |
| secret := secretRender{} |
| if err := json.Unmarshal(content, &secret); err != nil { |
| t.Fatal(err) |
| } |
| var expectedValues secretRender |
| if tc.expectedValues != nil { |
| expectedValues = *tc.expectedValues |
| } else { |
| expectedValues = secretRender{ |
| Username: "appuser", |
| Password: "password", |
| Version: "3", |
| } |
| } |
| if secret != expectedValues { |
| t.Fatalf("secret didn't match, expected: %#v, got: %#v", expectedValues, secret) |
| } |
| } |
| if len(errs) != 0 { |
| t.Fatalf("Failed to find the expected files. Expected %d, got %d\n\t%s", len(templatesToRender), fileCount, strings.Join(errs, "\n\t")) |
| } |
| }) |
| } |
| } |
| |
| // TestNewServerLogLevels tests that the server can be started with any log |
| // level. |
| func TestNewServerLogLevels(t *testing.T) { |
| ts := createHttpTestServer() |
| defer ts.Close() |
| |
| tmpDir, err := os.MkdirTemp("", "agent-tests") |
| defer os.RemoveAll(tmpDir) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| levels := []hclog.Level{hclog.NoLevel, hclog.Trace, hclog.Debug, hclog.Info, hclog.Warn, hclog.Error} |
| for _, level := range levels { |
| name := fmt.Sprintf("log_%s", level) |
| t.Run(name, func(t *testing.T) { |
| server := NewServer(&ServerConfig{ |
| Logger: logging.NewVaultLogger(level), |
| LogWriter: hclog.DefaultOutput, |
| LogLevel: level, |
| ExitAfterAuth: true, |
| AgentConfig: &config.Config{ |
| Vault: &config.Vault{ |
| Address: ts.URL, |
| }, |
| }, |
| }) |
| if server == nil { |
| t.Fatal("nil server returned") |
| } |
| defer server.Stop() |
| |
| templateTokenCh := make(chan string, 1) |
| |
| templateTest := &ctconfig.TemplateConfig{ |
| Contents: pointerutil.StringPtr(templateContents), |
| } |
| dstFile := fmt.Sprintf("%s/%s", tmpDir, name) |
| templateTest.Destination = pointerutil.StringPtr(dstFile) |
| templatesToRender := []*ctconfig.TemplateConfig{templateTest} |
| |
| ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) |
| defer cancel() |
| |
| errCh := make(chan error) |
| go func() { |
| errCh <- server.Run(ctx, templateTokenCh, templatesToRender) |
| }() |
| |
| // send a dummy value to trigger auth so the server will exit |
| templateTokenCh <- "test" |
| |
| select { |
| case <-ctx.Done(): |
| t.Fatal("timeout reached before templates were rendered") |
| case err := <-errCh: |
| if err != nil { |
| t.Fatalf("did not expect error, got: %v", err) |
| } |
| } |
| }) |
| } |
| } |
| |
| var jsonResponse = ` |
| { |
| "request_id": "8af096e9-518c-7351-eff5-5ba20554b21f", |
| "lease_id": "", |
| "renewable": false, |
| "lease_duration": 0, |
| "data": { |
| "data": { |
| "password": "password", |
| "username": "appuser" |
| }, |
| "metadata": { |
| "created_time": "2019-10-07T22:18:44.233247Z", |
| "deletion_time": "", |
| "destroyed": false, |
| "version": 3 |
| } |
| }, |
| "wrap_info": null, |
| "warnings": null, |
| "auth": null |
| } |
| ` |
| |
| var templateContents = ` |
| {{ with secret "kv/myapp/config"}} |
| { |
| {{ if .Data.data.username}}"username":"{{ .Data.data.username}}",{{ end }} |
| {{ if .Data.data.password }}"password":"{{ .Data.data.password }}",{{ end }} |
| {{ if .Data.metadata.version}}"version":"{{ .Data.metadata.version }}"{{ end }} |
| } |
| {{ end }} |
| ` |
| |
| var templateContentsMissingKey = ` |
| {{ with secret "kv/myapp/config"}} |
| { |
| {{ if .Data.data.foo}}"foo":"{{ .Data.data.foo}}"{{ end }} |
| } |
| {{ end }} |
| ` |
| |
| var templateContentsBad = ` |
| {{ with secret "kv/myapp/config-bad"}} |
| { |
| {{ if .Data.data.username}}"username":"{{ .Data.data.username}}",{{ end }} |
| {{ if .Data.data.password }}"password":"{{ .Data.data.password }}",{{ end }} |
| {{ if .Data.metadata.version}}"version":"{{ .Data.metadata.version }}"{{ end }} |
| } |
| {{ end }} |
| ` |
| |
| var templateContentsPermDenied = ` |
| {{ with secret "kv/myapp/perm-denied"}} |
| { |
| {{ if .Data.data.username}}"username":"{{ .Data.data.username}}",{{ end }} |
| {{ if .Data.data.password }}"password":"{{ .Data.data.password }}",{{ end }} |
| {{ if .Data.metadata.version}}"version":"{{ .Data.metadata.version }}"{{ end }} |
| } |
| {{ end }} |
| ` |
| |
| var templateContentsWithSprigFunctions = ` |
| {{ with secret "kv/myapp/config"}} |
| { |
| {{ if .Data.data.username}}"username":"{{ .Data.data.username | sprig_upper }}",{{ end }} |
| {{ if .Data.data.password }}"password":"{{ .Data.data.password | sprig_replace "word" "phrase" }}",{{ end }} |
| {{ if .Data.metadata.version}}"version":"{{ .Data.metadata.version }}"{{ end }} |
| } |
| {{ end }} |
| ` |